Attempt at polyfilling MariaDB functions for SQLite.
This commit is contained in:
parent
17cdb4d1c2
commit
2f577d97cc
4 changed files with 320 additions and 10 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2410.191603
|
||||
0.2410.211811
|
||||
|
|
18
composer.lock
generated
18
composer.lock
generated
|
@ -1324,16 +1324,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "11.4.1",
|
||||
"version": "11.4.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "7875627f15f4da7e7f0823d1f323f7295a77334e"
|
||||
"reference": "1863643c3f04ad03dcb9c6996c294784cdda4805"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7875627f15f4da7e7f0823d1f323f7295a77334e",
|
||||
"reference": "7875627f15f4da7e7f0823d1f323f7295a77334e",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1863643c3f04ad03dcb9c6996c294784cdda4805",
|
||||
"reference": "1863643c3f04ad03dcb9c6996c294784cdda4805",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1347,21 +1347,21 @@
|
|||
"phar-io/manifest": "^2.0.4",
|
||||
"phar-io/version": "^3.2.1",
|
||||
"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-invoker": "^5.0.1",
|
||||
"phpunit/php-text-template": "^4.0.1",
|
||||
"phpunit/php-timer": "^7.0.1",
|
||||
"sebastian/cli-parser": "^3.0.2",
|
||||
"sebastian/code-unit": "^3.0.1",
|
||||
"sebastian/comparator": "^6.1.0",
|
||||
"sebastian/comparator": "^6.1.1",
|
||||
"sebastian/diff": "^6.0.2",
|
||||
"sebastian/environment": "^7.2.0",
|
||||
"sebastian/exporter": "^6.1.3",
|
||||
"sebastian/global-state": "^7.0.2",
|
||||
"sebastian/object-enumerator": "^6.0.1",
|
||||
"sebastian/type": "^5.1.0",
|
||||
"sebastian/version": "^5.0.1"
|
||||
"sebastian/version": "^5.0.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-soap": "To be able to generate mocks based on WSDL files"
|
||||
|
@ -1404,7 +1404,7 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"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": [
|
||||
{
|
||||
|
@ -1420,7 +1420,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-10-08T15:38:37+00:00"
|
||||
"time": "2024-10-19T13:05:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
|
|
225
src/Db/Sqlite/SqliteMariaDbPolyfill.php
Normal file
225
src/Db/Sqlite/SqliteMariaDbPolyfill.php
Normal 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;
|
||||
}
|
||||
}
|
85
tests/SqliteMariaDbPolyfillTest.php
Normal file
85
tests/SqliteMariaDbPolyfillTest.php
Normal 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));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue