├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .scrutinizer.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src └── Vectorface │ └── MySQLite │ ├── MySQL │ ├── Aggregate.php │ ├── Comparison.php │ ├── DateTime.php │ ├── Flow.php │ ├── Numeric.php │ └── StringFunctions.php │ └── MySQLite.php └── tests └── Vectorface └── Tests └── MySQLite ├── MySQLiteTest.php └── Util └── FakePDO.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: php-actions/composer@run-as-current-user 12 | 13 | - name: PHPUnit Tests 14 | uses: php-actions/phpunit@v4 15 | with: 16 | bootstrap: vendor/autoload.php 17 | configuration: phpunit.xml 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | composer.phar 3 | coverage/ 4 | vendor/ 5 | .idea 6 | .phpunit.result.cache 7 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | nodes: 3 | analysis: 4 | tests: 5 | override: 6 | - php-scrutinizer-run 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Vectorface, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MySQLite 2 | ======== 3 | [![Build Status](https://github.com/Vectorface/MySQLite/actions/workflows/ci.yml/badge.svg)](https://github.com/Vectorface/MySQLite/actions) 4 | [![Code Coverage](https://scrutinizer-ci.com/g/Vectorface/MySQLite/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/Vectorface/MySQLite/?branch=master) 5 | 6 | MySQLite is an easy way to add MySQL functions to SQLite accessed through PDO. This can be useful for testing and development where an SQLite database may be more practical than a real MySQL database. 7 | 8 | Usage 9 | ----- 10 | 11 | Using MySQLite is meant to be a one-step affair, with no configuration required: 12 | 13 | ```php 14 | use Vectorface\MySQLite\MySQLite; 15 | 16 | // Create a PDO instance on an SQLite database 17 | $pdo = new PDO('sqlite::memory:'); 18 | 19 | // Create compatibility functions for use within that database connection. 20 | MySQLite::createFunctions($pdo); 21 | 22 | // Use it. 23 | $three = $pdo->query("SELECT BIT_OR(1, 2)")->fetch(PDO::FETCH_COLUMN); 24 | // Wait... That works now?!? What the what?!? 25 | ``` 26 | 27 | It is also possible to use it as a one-liner: 28 | 29 | ```php 30 | $pdo = MySQLite::createFunctions(new PDO('sqlite::memory:')); 31 | 32 | ``` 33 | 34 | You can get a list of supported functions with ```MySQLite::getFunctionList()```. 35 | 36 | 37 | Limitations 38 | ----------- 39 | 40 | MySQLite only provides a limited subset of MySQL functions. There are a lot of MySQL functions, so there's both a developer cost and performance penalty to adding them all. 41 | 42 | If you want to add more, it's easy. You only need to add a function called mysql_[function name] into one of the traits in src/Vectorface/MySQLite/MySQL. 43 | 44 | For example, to create a function ```FOO()``` with silly behavior in String.php: 45 | 46 | ```php 47 | ... 48 | public static function mysql_foo($foo) 49 | { 50 | if ($foo == "foo") { 51 | return "bar"; 52 | } 53 | return $foo; 54 | } 55 | ... 56 | ``` 57 | 58 | With the above definition present, String::mysql_foo will automatically be registered with ```MySQLite::createFunctions($pdo)``` is used. 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vectorface/mysqlite", 3 | "description": "Select MySQL compatibility functions for PDO's SQLite driver", 4 | "keywords": ["SQLite", "MySQL", "debug", "testing", "compatibility"], 5 | "type": "library", 6 | "license": "MIT", 7 | "autoload": { 8 | "psr-4": { 9 | "Vectorface\\MySQLite\\": "src/Vectorface/MySQLite/" 10 | } 11 | }, 12 | "autoload-dev": { 13 | "psr-4": { 14 | "Vectorface\\Tests\\MySQLite\\": "tests/Vectorface/Tests/MySQLite/" 15 | } 16 | }, 17 | "homepage": "https://github.com/Vectorface/MySQLite", 18 | "support": { 19 | "issues": "https://github.com/Vectorface/MySQLite/issues", 20 | "source": "https://github.com/Vectorface/MySQLite" 21 | }, 22 | "require": { 23 | "php": "^8.0.0", 24 | "ext-pdo": "*" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^9", 28 | "squizlabs/php_codesniffer": "~2.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./ 6 | 7 | 8 | vendor 9 | tests 10 | 11 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Vectorface/MySQLite/MySQL/Aggregate.php: -------------------------------------------------------------------------------- 1 | getAttribute(PDO::ATTR_DRIVER_NAME) !== 'sqlite') { 59 | throw new InvalidArgumentException('Expecting a PDO instance using the SQLite driver'); 60 | } 61 | 62 | foreach (static::getPublicMethodData() as $method => $paramCount) { 63 | static::registerMethod($pdo, $method, $paramCount, $fnList); 64 | } 65 | 66 | return $pdo; 67 | } 68 | 69 | /** 70 | * Get information about functions that are meant to be exposed by this class. 71 | * 72 | * @return int[] An associative array composed of function names mapping to accepted parameter counts. 73 | */ 74 | protected static function getPublicMethodData() 75 | { 76 | $data = []; 77 | 78 | $ref = new ReflectionClass(__CLASS__); 79 | $methods = $ref->getMethods(ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC); 80 | foreach ($methods as $method) { 81 | if (strpos($method->name, 'mysql_') !== 0) { 82 | continue; 83 | } 84 | 85 | $data[$method->name] = $method->getNumberOfRequiredParameters(); 86 | } 87 | 88 | return $data; 89 | } 90 | 91 | /** 92 | * Register a method as an SQLite funtion 93 | * 94 | * @param PDO &$pdo A PDO instance to which the MySQLite compatibility functions should be added. 95 | * @param string $method The internal method name. 96 | * @param int $paramCount The suggested parameter count. 97 | * @param string[] $fnList A list of functions to create on the SQLite database, or empty for all. 98 | * @return bool Returns true if the method was registed. False otherwise. 99 | */ 100 | protected static function registerMethod(PDO &$pdo, $method, $paramCount, ?array $fnList = null) 101 | { 102 | $function = substr($method, 6); /* Strip 'mysql_' prefix to get the function name. */ 103 | 104 | /* Skip functions not in the list. */ 105 | if (!empty($fnList) && !in_array($function, $fnList)) { 106 | return false; 107 | } 108 | 109 | if ($paramCount) { 110 | return $pdo->sqliteCreateFunction($function, [__CLASS__, $method], $paramCount); 111 | } 112 | return $pdo->sqliteCreateFunction($function, [__CLASS__, $method]); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/Vectorface/Tests/MySQLite/MySQLiteTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(1 | 2 | 4, MySQLite::mysql_bit_or(1, 2, 4)); 24 | 25 | /* Comparison functions */ 26 | $this->assertEquals(1, MySQLite::mysql_least(1, 2, 3, 4)); 27 | try { 28 | MySQLite::mysql_least(); 29 | $this->fail("Least with no arguments is not valid"); 30 | } catch (\InvalidArgumentException $e) { 31 | /* Expected */ 32 | } 33 | 34 | /* Flow control functions */ 35 | $this->assertEquals("foo", MySQLite::mysql_if(true, "foo", "bar")); 36 | $this->assertEquals("bar", MySQLite::mysql_if(false, "foo", "bar")); 37 | 38 | /* Numeric functions */ 39 | $this->assertEquals(10, MySQLite::mysql_sqrt(100)); 40 | } 41 | 42 | public function testDateTimeFunctions() 43 | { 44 | $this->assertEquals(date("Y-m-d H:i:s"), MySQLite::mysql_now()); 45 | $this->assertEquals(365, MySQLite::mysql_to_days("0000-12-31")); 46 | $this->assertEquals(718613, MySQLite::mysql_to_days("1967-07-01")); 47 | $this->assertEquals(735599, MySQLite::mysql_to_days("2014-01-01")); 48 | $this->assertEquals(time(), MySQLite::mysql_unix_timestamp()); 49 | $this->assertEquals(0, MySQLite::mysql_unix_timestamp("0000-00-00 00:00:00")); 50 | $this->assertEquals(0, MySQLite::mysql_unix_timestamp("0000-00-00")); 51 | $time = time(); 52 | $this->assertEquals($time, MySQLite::mysql_unix_timestamp(date("Y-m-d H:i:s"))); 53 | } 54 | 55 | /** 56 | * Test that createFunctions hooks the functions into a PDO object. 57 | */ 58 | public function testCreateFunctions() 59 | { 60 | $fakepdo = new FakePDO(); 61 | $fakepdo->attributes[PDO::ATTR_DRIVER_NAME] = 'mysql'; 62 | 63 | try { 64 | MySQLite::createFunctions($fakepdo); 65 | $this->fail("Attempt to create functions with a driver other than SQLite should fail."); 66 | } catch (InvalidArgumentException $e) { 67 | /* Expected */ 68 | } 69 | 70 | $pdo = new PDO("sqlite::memory:", null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); 71 | try { 72 | $pdo->query("SELECT BIT_OR(1, 2)"); 73 | $this->fail("Attempt to BIT_OR two values is expected to fail before the function is created."); 74 | } catch (PDOException $e) { 75 | /* Expected */ 76 | } 77 | 78 | $this->assertSame($pdo, MySQLite::createFunctions($pdo)); 79 | $this->assertEquals(3, $pdo->query("SELECT BIT_OR(1, 2)")->fetch(PDO::FETCH_COLUMN)); 80 | } 81 | 82 | /** 83 | * Test that createFunctions is able to create only a limited subset of supported functions. 84 | */ 85 | public function testSelectiveCreateFunctions() 86 | { 87 | $pdo = new PDO("sqlite::memory:", null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); 88 | $this->assertSame($pdo, MySQLite::createFunctions($pdo, ['bit_or'])); 89 | $this->assertEquals(3, $pdo->query("SELECT BIT_OR(1, 2)")->fetch(PDO::FETCH_COLUMN)); 90 | try { 91 | $pdo->query("SELECT UNIX_TIMESTAMP()"); 92 | $this->fail("UNIX_TIMESTAMP function is expected not to have been created."); 93 | } catch (PDOException $e) { 94 | /* Expected */ 95 | } 96 | } 97 | 98 | /** 99 | * Test that registered functions are listed and available. 100 | */ 101 | public function testGetFunctionList() 102 | { 103 | $this->assertContains("bit_or", MySQLite::getFunctionList()); 104 | $this->assertContains("unix_timestamp", MySQLite::getFunctionList()); 105 | } 106 | 107 | /** 108 | * Test the concat function 109 | */ 110 | public function testConcat() 111 | { 112 | $expected = 'test1 test2 test4'; 113 | $test = MySQLite::mysql_concat("test1", " ", "test2", " ", "test4"); 114 | $this->assertEquals($expected, $test); 115 | 116 | $pdo = new PDO("sqlite::memory:", null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); 117 | MySQLite::createFunctions($pdo); 118 | $result = $pdo->query('SELECT CONCAT("test1"," ","test2"," " ,"test4")')->fetch(PDO::FETCH_COLUMN); 119 | $this->assertEquals($expected, $result); 120 | } 121 | 122 | /** 123 | * Test the concat_ws function 124 | */ 125 | public function testConcatWS() 126 | { 127 | $expected = 'test1|test2|test4'; 128 | $test = MySQLite::mysql_concat_ws("|", "test1", "test2", "test4"); 129 | $this->assertEquals($expected, $test); 130 | 131 | $pdo = new PDO("sqlite::memory:", null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); 132 | MySQLite::createFunctions($pdo); 133 | $result = $pdo->query('SELECT CONCAT_WS("|","test1","test2","test4")')->fetch(PDO::FETCH_COLUMN); 134 | $this->assertEquals($expected, $result); 135 | } 136 | 137 | /** 138 | * Test the rand function 139 | */ 140 | public function testRand() 141 | { 142 | $pdo = new PDO("sqlite::memory:", null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); 143 | MySQLite::createFunctions($pdo); 144 | 145 | $pdo->exec("CREATE TABLE testing(id INT PRIMARY KEY NOT NULL)"); 146 | $stmt = $pdo->prepare("INSERT INTO testing (id) VALUES (?)"); 147 | for ($x = 0; $x <= 10; $x++) { 148 | $stmt->execute([$x]); 149 | } 150 | 151 | $results = []; 152 | for ($x = 0; $x < 20; $x++) { 153 | $results[] = $pdo->query('SELECT id FROM testing ORDER BY RAND() LIMIT 1')->fetch(PDO::FETCH_COLUMN); 154 | } 155 | 156 | $this->assertNotEquals( 157 | array_slice($results, 0, 10), 158 | array_slice($results, 10, 10) 159 | ); 160 | } 161 | 162 | public function testFormat() 163 | { 164 | $expected = '12,312,312'; 165 | $test = MySQLite::mysql_format("12312312.232", 0); 166 | $this->assertEquals($expected, $test); 167 | 168 | $expected = '12.2'; 169 | $test = MySQLite::mysql_format("12.232", 1); 170 | $this->assertEquals($expected, $test); 171 | } 172 | 173 | public function testSoundex() 174 | { 175 | $this->assertEquals("F000", MySQLite::mysql_soundex("foo")); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /tests/Vectorface/Tests/MySQLite/Util/FakePDO.php: -------------------------------------------------------------------------------- 1 | attributes[$attr]) ? $this->attributes[$attr] : parent::getAttribute($attr); 35 | } 36 | } 37 | --------------------------------------------------------------------------------