diff --git a/VERSION b/VERSION index 6380571..83e07c1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2410.191603 +0.2410.211811 diff --git a/composer.lock b/composer.lock index dd66e50..c5ccd3a 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/src/Db/Sqlite/SqliteMariaDbPolyfill.php b/src/Db/Sqlite/SqliteMariaDbPolyfill.php new file mode 100644 index 0000000..d5e2cd5 --- /dev/null +++ b/src/Db/Sqlite/SqliteMariaDbPolyfill.php @@ -0,0 +1,225 @@ +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; + } +} diff --git a/tests/SqliteMariaDbPolyfillTest.php b/tests/SqliteMariaDbPolyfillTest.php new file mode 100644 index 0000000..2a52885 --- /dev/null +++ b/tests/SqliteMariaDbPolyfillTest.php @@ -0,0 +1,85 @@ +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)); + } +}