Attempt at polyfilling MariaDB functions for SQLite.

This commit is contained in:
flash 2024-10-21 18:12:23 +00:00
parent 17cdb4d1c2
commit 2f577d97cc
4 changed files with 320 additions and 10 deletions

View file

@ -1 +1 @@
0.2410.191603 0.2410.211811

18
composer.lock generated
View file

@ -1324,16 +1324,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "11.4.1", "version": "11.4.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "7875627f15f4da7e7f0823d1f323f7295a77334e" "reference": "1863643c3f04ad03dcb9c6996c294784cdda4805"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7875627f15f4da7e7f0823d1f323f7295a77334e", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1863643c3f04ad03dcb9c6996c294784cdda4805",
"reference": "7875627f15f4da7e7f0823d1f323f7295a77334e", "reference": "1863643c3f04ad03dcb9c6996c294784cdda4805",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1347,21 +1347,21 @@
"phar-io/manifest": "^2.0.4", "phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1", "phar-io/version": "^3.2.1",
"php": ">=8.2", "php": ">=8.2",
"phpunit/php-code-coverage": "^11.0.6", "phpunit/php-code-coverage": "^11.0.7",
"phpunit/php-file-iterator": "^5.1.0", "phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-invoker": "^5.0.1", "phpunit/php-invoker": "^5.0.1",
"phpunit/php-text-template": "^4.0.1", "phpunit/php-text-template": "^4.0.1",
"phpunit/php-timer": "^7.0.1", "phpunit/php-timer": "^7.0.1",
"sebastian/cli-parser": "^3.0.2", "sebastian/cli-parser": "^3.0.2",
"sebastian/code-unit": "^3.0.1", "sebastian/code-unit": "^3.0.1",
"sebastian/comparator": "^6.1.0", "sebastian/comparator": "^6.1.1",
"sebastian/diff": "^6.0.2", "sebastian/diff": "^6.0.2",
"sebastian/environment": "^7.2.0", "sebastian/environment": "^7.2.0",
"sebastian/exporter": "^6.1.3", "sebastian/exporter": "^6.1.3",
"sebastian/global-state": "^7.0.2", "sebastian/global-state": "^7.0.2",
"sebastian/object-enumerator": "^6.0.1", "sebastian/object-enumerator": "^6.0.1",
"sebastian/type": "^5.1.0", "sebastian/type": "^5.1.0",
"sebastian/version": "^5.0.1" "sebastian/version": "^5.0.2"
}, },
"suggest": { "suggest": {
"ext-soap": "To be able to generate mocks based on WSDL files" "ext-soap": "To be able to generate mocks based on WSDL files"
@ -1404,7 +1404,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.4.1" "source": "https://github.com/sebastianbergmann/phpunit/tree/11.4.2"
}, },
"funding": [ "funding": [
{ {
@ -1420,7 +1420,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-10-08T15:38:37+00:00" "time": "2024-10-19T13:05:19+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",

View file

@ -0,0 +1,225 @@
<?php
// SqliteMariaDbPolyfill.php
// Created: 2024-10-21
// Updated: 2024-10-21
namespace Index\Db\Sqlite;
use DateTimeImmutable;
use RuntimeException;
use Random\Randomizer;
use Random\Engine\Mt19937;
/**
* Provides implementations of MariaDB functions for use in SQLite.
*/
final class SqliteMariaDbPolyfill {
/**
* @throws RuntimeException Will always throw because this is a static class exclusively.
*/
public function __construct() {
throw new RuntimeException('This is a static class, you cannot create an instance of it.');
}
/**
* Adds all functions to a given SQLite connection.
*
* @param SqliteConnection $conn Connection to add functions to.
*/
public static function apply(SqliteConnection $conn): void {
$conn->createFunction('IF', self::mdbIf(...));
$conn->createFunction('NOW', self::mdbNow(...));
$conn->createFunction('RAND', self::mdbRand(...));
$conn->createFunction('UNIX_TIMESTAMP', self::mdbUnixTimestamp(...));
$conn->createFunction('FROM_UNIXTIME', self::mdbFromUnixtime(...));
$conn->createFunction('INET6_NTOA', self::mdbInet6ntoa(...));
$conn->createFunction('INET6_ATON', self::mdbInet6aton(...));
$conn->createFunction('UNHEX', self::mdbUnhex(...));
}
/**
* MariaDB compatible IF function.
*
* @see https://mariadb.com/kb/en/if-function/
* @param mixed $condition Condition that was tested.
* @param mixed $true Return value if true ($condition <> 0 and $condition <> null)
* @param mixed $false Return value if false.
* @return mixed Either $true or $false.
*/
public static function mdbIf(mixed $condition, mixed $true, mixed $false): mixed {
return $condition ? $true : $false;
}
/**
* Returns a number in the range 0 <= v < 1.0.
*
* @see https://mariadb.com/kb/en/rand/
* @param mixed $seed Seed value.
* @return float Random number.
*/
public static function mdbRand(mixed $seed = null): float {
return (new Randomizer(new Mt19937(is_scalar($seed) ? (int)$seed : null)))->nextFloat();
}
/**
* MariaDB compatible NOW/CURRENT_TIMESTAMP function.
*
* Returns current date/time in YYYY-MM-DD HH:MM:SS[.uuuuuu] format.
* Does not support numeric mode.
*
* @see https://mariadb.com/kb/en/now/
* @param mixed $precision Millisecond precision.
* @return string Date.
*/
public static function mdbNow(mixed $precision = null): mixed {
$dt = new DateTimeImmutable('now');
$precision = is_scalar($precision) ? min(6, max(0, (int)$precision)) : 0;
$ts = $dt->format('Y-m-d H:i:s');
if($precision > 0)
$ts .= '.' . substr($dt->format('u'), 0, $precision);
return $ts;
}
/**
* Returns the current UNIX timestamp or gets it from a date/time string.
*
* @see https://mariadb.com/kb/en/unix_timestamp/
* @param mixed $arg Null or a scalar value.
* @return int|float Timestamp.
*/
public static function mdbUnixTimestamp(mixed $arg = null): int|float {
if(is_scalar($arg)) {
$dt = new DateTimeImmutable((string)$arg);
$ts = $dt->format('U');
$ms = $dt->format('u');
if(trim($ms, '0') !== '')
return (float)sprintf('%d.%d', $ts, $ms);
return (int)$ts;
}
return time();
}
/**
* Returns the current UNIX timestamp or gets it from a date/time string.
*
* @see https://mariadb.com/kb/en/from_unixtime/
* @param mixed $ts Timestamp.
* @param mixed $format Format string like in the specified MariaDB config.
* @return ?string Formatted timestamp.
*/
public static function mdbFromUnixtime(mixed $ts, mixed $format = null): ?string {
if(!is_scalar($ts))
return null;
$ts = (float)$ts;
$hasMs = abs($ts - (int)$ts) > 0;
$ts = new DateTimeImmutable(sprintf('@%F', $ts));
if(is_scalar($format)) {
$format = strtr((string)$format, [
'%a' => 'D',
'%b' => 'M',
'%c' => 'n',
'%D' => 'jS',
'%d' => 'd',
'%e' => 'j',
'%f' => 'u',
'%H' => 'H',
'%h' => 'h',
'%I' => 'H',
'%i' => 'i',
'%j' => 'z', // uh-oh this should be +1
'%k' => 'G',
'%l' => 'g',
'%M' => 'F',
'%m' => 'm',
'%p' => 'A',
'%r' => 'g:i:s A',
'%S' => 's',
'%s' => 's',
'%T' => 'H:i:s',
'%U' => 'W', // whoops these are also all approximations
'%u' => 'W', // ^
'%V' => 'W', // ^
'%v' => 'W', // ^
'%W' => 'l',
'%w' => 'w',
'%X' => 'Y', // also wrong
'%x' => 'Y', // ^
'%Y' => 'Y',
'%y' => 'y',
'%#' => '#', // unsupported
'%.' => '.', // ^
'%@' => '@', // ^
'%%' => '%',
]);
} else {
$format = 'Y-m-d H:i:s';
if($hasMs)
$format .= '.u';
}
return $ts->format($format);
}
/**
* Converts a binary IPv4 or IPv6 address and converts it to a human readable representation.
*
* @see https://mariadb.com/kb/en/inet6_ntoa/
* @param mixed $addr Binary IP address.
* @return ?string Formatted IP address or NULL if the input could not be understood.
*/
public static function mdbInet6ntoa(mixed $addr): ?string {
if(!is_scalar($addr))
return null;
$addr = inet_ntop((string)$addr);
if($addr === false)
return null;
return $addr;
}
/**
* Converts a human readable IPv4 or IPv6 address and converts it to a binary representation.
*
* @see https://mariadb.com/kb/en/inet6_aton/
* @param mixed $addr Human readable IP address.
* @return ?string Binary IP address or NULL if the input was not understood.
*/
public static function mdbInet6aton(mixed $addr): ?string {
if(!is_scalar($addr))
return null;
$addr = inet_pton((string)$addr);
if($addr === false)
return null;
return $addr;
}
/**
* Unhex function.
*
* HEX() is defined but UNHEX() isn't???
*
* @see https://mariadb.com/kb/en/unhex/
* @param mixed $str Hex string.
* @return mixed Binary value.
*/
public static function mdbUnhex(mixed $str): ?string {
if(!is_scalar($str))
return null;
$str = hex2bin($str);
if($str === false)
return '';
return $str;
}
}

View file

@ -0,0 +1,85 @@
<?php
// SqliteMariaDbPolyfillTest.php
// Created: 2024-10-21
// Updated: 2024-10-21
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\{CoversClass,UsesClass};
use Index\Db\Sqlite\{SqliteConnection,SqliteConnectionInfo,SqliteMariaDbPolyfill};
#[CoversClass(SqliteMariaDbPolyfill::class)]
#[UsesClass(SqliteConnection::class)]
#[UsesClass(SqliteConnectionInfo::class)]
final class SqliteMariaDbPolyfillTest extends TestCase {
public function testSqliteMariaDbPolyfill(): void {
$dbPath = tempnam(sys_get_temp_dir(), 'ndx');
$db = new SqliteConnection(SqliteConnectionInfo::createTemp());
SqliteMariaDbPolyfill::apply($db);
$result = $db->query('SELECT IF(1, "true", "false"), IF(0, "true", "false"), IF(NULL, "true", "false"), IF("beans", "true", "false")');
$this->assertTrue($result->next());
$this->assertEquals('true', $result->getString(0));
$this->assertEquals('false', $result->getString(1));
$this->assertEquals('false', $result->getString(2));
$this->assertEquals('true', $result->getString(3));
$result = $db->query('SELECT NOW(), NOW(1), NOW(2), NOW(3), NOW(4), NOW(5), NOW(6), NOW(7)');
$this->assertTrue($result->next());
$this->assertEquals(date('Y-m-d H:i:s'), $result->getString(0)); // lol
$this->assertEquals(21, strlen($result->getString(1)));
$this->assertEquals(22, strlen($result->getString(2)));
$this->assertEquals(23, strlen($result->getString(3)));
$this->assertEquals(24, strlen($result->getString(4)));
$this->assertEquals(25, strlen($result->getString(5)));
$this->assertEquals(26, strlen($result->getString(6)));
$this->assertEquals(26, strlen($result->getString(7)));
// the implementation isn't identical to MariaDB but it does return the same values for a given seed
$result = $db->query('SELECT RAND(1337), RAND(1337), RAND(6770), RAND(25252)');
$this->assertTrue($result->next());
$this->assertEquals('0.5605297529283', $result->getString(0));
$this->assertEquals('0.5605297529283', $result->getString(1));
$this->assertEquals('0.19382955825286', $result->getString(2));
$this->assertEquals('0.76407597350729', $result->getString(3));
$result = $db->query('SELECT UNIX_TIMESTAMP(), UNIX_TIMESTAMP("2013-01-27 22:12:34"), UNIX_TIMESTAMP("2013-01-27 22:12:34.567891")');
$this->assertTrue($result->next());
$this->assertEquals(time(), $result->getInteger(0));
$this->assertEquals(1359324754, $result->getInteger(1));
$this->assertEquals(1359324754.567891, $result->getFloat(2));
$result = $db->query('SELECT UNIX_TIMESTAMP(), UNIX_TIMESTAMP("2013-01-27 22:12:34"), UNIX_TIMESTAMP("2013-01-27 22:12:34.567891")');
$this->assertTrue($result->next());
$this->assertEquals(time(), $result->getInteger(0));
$this->assertEquals(1359324754, $result->getInteger(1));
$this->assertEquals(1359324754.567891, $result->getFloat(2));
$result = $db->query('SELECT HEX(INET6_ATON("::1")), HEX(INET6_ATON("127.0.0.1")), INET6_ATON("Beans"), INET6_ATON(NULL), HEX(INET6_ATON("86.81.56.51")), HEX(INET6_ATON("2a02:a445:c012:1:690b:2fa4:4009:e06"))');
$this->assertTrue($result->next());
$this->assertEquals('00000000000000000000000000000001', $result->getString(0));
$this->assertEquals('7F000001', $result->getString(1));
$this->assertTrue($result->isNull(2));
$this->assertTrue($result->isNull(3));
$this->assertEquals('56513833', $result->getString(4));
$this->assertEquals('2A02A445C0120001690B2FA440090E06', $result->getString(5));
$result = $db->query('SELECT INET6_NTOA(UNHEX("00000000000000000000000000000001")), INET6_NTOA(UNHEX("7f000001")), INET6_NTOA("Bean"), INET6_NTOA("Beans"), INET6_NTOA(NULL), INET6_NTOA(UNHEX("56513833")), INET6_NTOA(UNHEX("2A02A445C0120001690B2FA440090E06"))');
$this->assertTrue($result->next());
$this->assertEquals('::1', $result->getString(0));
$this->assertEquals('127.0.0.1', $result->getString(1));
$this->assertEquals('66.101.97.110', $result->getString(2));
$this->assertTrue($result->isNull(3));
$this->assertTrue($result->isNull(4));
$this->assertEquals('86.81.56.51', $result->getString(5));
$this->assertEquals('2a02:a445:c012:1:690b:2fa4:4009:e06', $result->getString(6));
$result = $db->query('SELECT FROM_UNIXTIME(NULL), FROM_UNIXTIME(1359324754), FROM_UNIXTIME(1359324754.567891), FROM_UNIXTIME(1359324754.567891, "%a %b %c %D %d %e %f %H %h %I %i %j %k %l %M %m %p %r %S %s %T %U %u %V %v %W %w %X %x %Y %y %# %. %@ %%")');
$this->assertTrue($result->next());
$this->assertTrue($result->isNull(0));
$this->assertEquals('2013-01-27 22:12:34', $result->getString(1));
$this->assertEquals('2013-01-27 22:12:34.567891', $result->getString(2));
$this->assertEquals('Sun Jan 1 27th 27 27 567891 22 10 22 12 26 22 10 January 01 PM 10:12:34 PM 34 34 22:12:34 04 04 04 04 Sunday 0 2013 2013 2013 13 # . @ %', $result->getString(3));
}
}