├── .nvmrc ├── .phprc ├── .gitignore ├── package.json ├── tests ├── phpunit │ ├── LogMysqlTest.php │ ├── BasicMysqlTest.php │ ├── BasicSqliteTest.php │ ├── LogPostgresTest.php │ ├── BasicPostgresTest.php │ ├── CredentialsSqlite.php │ ├── CredentialsMysql.php │ ├── CredentialsPostgres.php │ ├── BasicSetup.php │ ├── LogSetup.php │ ├── LogTest.php │ └── BasicTest.php └── lock │ ├── insert.php │ └── run.php ├── .prettierrc ├── phpunit.xml ├── composer.json ├── .github └── workflows │ └── ci.yml ├── src ├── static.php └── dbhelper.php └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.phprc: -------------------------------------------------------------------------------- 1 | 8.3 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | composer.lock 3 | /vendor/ 4 | package-lock.json 5 | /node_modules/ 6 | /.phpunit.result.cache 7 | /bin/act -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@prettier/plugin-php": ">=0.22", 4 | "prettier": ">=3" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/phpunit/LogMysqlTest.php: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | tests/phpunit 13 | 14 | 15 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vielhuber/dbhelper", 3 | "description": "Small PHP wrapper for mysql/pgsql databases.", 4 | "require": { 5 | "php": ">=7.4" 6 | }, 7 | "license": "MIT", 8 | "autoload": { 9 | "psr-4": { 10 | "vielhuber\\dbhelper\\": "src/", 11 | "Tests\\Phpunit\\": "tests/phpunit/" 12 | } 13 | }, 14 | "require-dev": { 15 | "phpunit/phpunit": "^9" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/phpunit/CredentialsSqlite.php: -------------------------------------------------------------------------------- 1 | 'pdo', 12 | 'engine' => 'sqlite', 13 | 'host' => 'dbhelper.db', 14 | 'username' => null, 15 | 'password' => null, 16 | 'port' => null, 17 | 'database' => null 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/phpunit/CredentialsMysql.php: -------------------------------------------------------------------------------- 1 | 'pdo', 12 | 'engine' => 'mysql', 13 | 'host' => '127.0.0.1', 14 | 'username' => 'root', 15 | 'password' => 'root', 16 | 'port' => 3306, 17 | 'database' => 'dbhelper' 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/phpunit/CredentialsPostgres.php: -------------------------------------------------------------------------------- 1 | 'pdo', 12 | 'engine' => 'postgres', 13 | 'host' => '127.0.0.1', 14 | 'username' => 'postgres', 15 | 'password' => 'root', 16 | 'port' => 5432, 17 | 'database' => 'dbhelper' 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/lock/insert.php: -------------------------------------------------------------------------------- 1 | connect('pdo', 'sqlite', __DIR__ . '/test.db', null, null, null, null, $argv[1]); 7 | if ($argv[2] == '1') { 8 | $db->query('PRAGMA locking_mode = EXCLUSIVE;'); 9 | $db->query('BEGIN EXCLUSIVE;'); 10 | sleep(10); 11 | $db->query('COMMIT;'); 12 | } else { 13 | $db->insert('test', [ 14 | 'col1' => $argv[2] 15 | ]); 16 | } 17 | $db->disconnect(); 18 | die('OK'); 19 | -------------------------------------------------------------------------------- /tests/lock/run.php: -------------------------------------------------------------------------------- 1 | connect_with_create('pdo', 'sqlite', __DIR__ . '/test.db', null, null, null, null, $argv[1]); 7 | $db->clear(); 8 | $db->create_table('test', [ 9 | 'id' => 'INTEGER PRIMARY KEY', 10 | 'col1' => 'TEXT', 11 | 'col2' => 'TEXT' 12 | ]); 13 | $iterations = 5; 14 | for ($i = 1; $i <= $iterations; $i++) { 15 | if ($i == 2) { 16 | sleep(1); 17 | } 18 | shell_exec('php ' . __DIR__ . '/insert.php ' . $argv[1] . ' ' . $i . ' > ' . __DIR__ . '/' . $i . '.log 2>&1 &'); 19 | } 20 | $finish = false; 21 | while ($finish === false) { 22 | $finish = true; 23 | for ($i = 1; $i <= $iterations; $i++) { 24 | if (!file_exists(__DIR__ . '/' . $i . '.log') || trim(file_get_contents(__DIR__ . '/' . $i . '.log')) == '') { 25 | sleep(0.1); 26 | $finish = false; 27 | } 28 | } 29 | } 30 | for ($i = 1; $i <= $iterations; $i++) { 31 | $content = file_get_contents(__DIR__ . '/' . $i . '.log'); 32 | echo $content . PHP_EOL; 33 | @unlink(__DIR__ . '/' . $i . '.log'); 34 | } 35 | $db->disconnect_with_delete(); 36 | -------------------------------------------------------------------------------- /tests/phpunit/BasicSetup.php: -------------------------------------------------------------------------------- 1 | connect_with_create( 17 | self::$credentials->driver, 18 | self::$credentials->engine, 19 | self::$credentials->host, 20 | self::$credentials->username, 21 | self::$credentials->password, 22 | self::$credentials->database, 23 | self::$credentials->port 24 | ); 25 | } 26 | 27 | public static function tearDownAfterClass(): void 28 | { 29 | self::$db->disconnect_with_delete(); 30 | } 31 | 32 | function setUp(): void 33 | { 34 | self::$db->clear(); // if something failed 35 | self::$db->create_table('test', [ 36 | 'id' => (self::$credentials->engine === 'sqlite' ? 'INTEGER' : 'SERIAL') . ' PRIMARY KEY', 37 | 'col1' => 'varchar(255)', 38 | 'col2' => 'varchar(255)', 39 | 'col3' => 'varchar(255)' 40 | ]); 41 | } 42 | 43 | function tearDown(): void 44 | { 45 | self::$db->clear(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | env: 4 | CI: true 5 | 6 | on: 7 | workflow_dispatch: 8 | push: 9 | tags: 10 | - 'v*' 11 | - '[0-9]*' 12 | 13 | jobs: 14 | build-test: 15 | runs-on: ${{ matrix.operating-system }} 16 | 17 | strategy: 18 | fail-fast: true 19 | matrix: 20 | operating-system: ['ubuntu-latest'] 21 | php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3'] 22 | 23 | name: ${{ matrix.operating-system }} (PHP ${{ matrix.php-versions }}) 24 | 25 | services: 26 | postgres: 27 | image: postgres 28 | env: 29 | POSTGRES_USER: postgres 30 | POSTGRES_PASSWORD: root 31 | ports: 32 | - 5432:5432 33 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 34 | 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v3 38 | 39 | - name: MySQL 40 | run: | 41 | sudo systemctl start mysql.service 42 | 43 | - name: PHP 44 | uses: shivammathur/setup-php@v2 45 | with: 46 | php-version: '${{ matrix.php-versions }}' 47 | tools: composer, phpunit 48 | 49 | - name: Composer 50 | run: composer install --no-interaction 51 | 52 | - name: PHPUnit 53 | run: ./vendor/bin/phpunit 54 | -------------------------------------------------------------------------------- /tests/phpunit/LogSetup.php: -------------------------------------------------------------------------------- 1 | true, 15 | 'logging_table' => 'logs', 16 | 'exclude' => [ 17 | 'tables' => ['test2'], 18 | 'columns' => ['test' => ['col4']] 19 | ], 20 | 'delete_older' => 12, 21 | 'updated_by' => 42 22 | ]); 23 | self::$credentials = self::getCredentials(); 24 | self::$db->connect_with_create( 25 | self::$credentials->driver, 26 | self::$credentials->engine, 27 | self::$credentials->host, 28 | self::$credentials->username, 29 | self::$credentials->password, 30 | self::$credentials->database, 31 | self::$credentials->port 32 | ); 33 | } 34 | 35 | public static function tearDownAfterClass(): void 36 | { 37 | self::$db->disconnect_with_delete(); 38 | } 39 | 40 | function setUp(): void 41 | { 42 | self::$db->clear(); // if something failed 43 | self::$db->create_table('test', [ 44 | 'id' => 'SERIAL PRIMARY KEY', 45 | 'col1' => 'varchar(255)', 46 | 'col2' => 'TEXT', 47 | 'col3' => 'int', 48 | 'col4' => 'varchar(255)' 49 | ]); 50 | self::$db->create_table('test2', [ 51 | 'id' => 'SERIAL PRIMARY KEY', 52 | 'col1' => 'varchar(255)', 53 | 'col2' => 'TEXT', 54 | 'col3' => 'int', 55 | 'col4' => 'varchar(255)' 56 | ]); 57 | self::$db->setup_logging(); 58 | self::$db->enable_auto_inject(); 59 | } 60 | 61 | function tearDown(): void 62 | { 63 | self::$db->clear(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/static.php: -------------------------------------------------------------------------------- 1 | connect(...$args); 6 | } 7 | function db_connect_with_create(...$args) 8 | { 9 | global $db; 10 | return $db->connect_with_create(...$args); 11 | } 12 | function db_create_database(...$args) 13 | { 14 | global $db; 15 | return $db->create_database(...$args); 16 | } 17 | function db_disconnect_with_delete(...$args) 18 | { 19 | global $db; 20 | return $db->disconnect_with_delete(...$args); 21 | } 22 | function db_delete_database(...$args) 23 | { 24 | global $db; 25 | return $db->delete_database(...$args); 26 | } 27 | function db_fetch_var(...$query) 28 | { 29 | global $db; 30 | return $db->fetch_var(...$query); 31 | } 32 | function db_fetch_row(...$query) 33 | { 34 | global $db; 35 | return $db->fetch_row(...$query); 36 | } 37 | function db_fetch_col(...$query) 38 | { 39 | global $db; 40 | return $db->fetch_col(...$query); 41 | } 42 | function db_fetch_all(...$query) 43 | { 44 | global $db; 45 | return $db->fetch_all(...$query); 46 | } 47 | function db_query(...$query) 48 | { 49 | global $db; 50 | return $db->query(...$query); 51 | } 52 | function db_insert($table, $data) 53 | { 54 | global $db; 55 | return $db->insert($table, $data); 56 | } 57 | function db_update($table, $data, $condition = null) 58 | { 59 | global $db; 60 | return $db->update($table, $data, $condition); 61 | } 62 | function db_delete($table, $conditions) 63 | { 64 | global $db; 65 | return $db->delete($table, $conditions); 66 | } 67 | function db_count($table, $condition = []) 68 | { 69 | global $db; 70 | return $db->count($table, $condition); 71 | } 72 | function db_last_insert_id() 73 | { 74 | global $db; 75 | return $db->last_insert_id(); 76 | } 77 | function db_disconnect() 78 | { 79 | global $db; 80 | return $db->disconnect(); 81 | } 82 | function db_clear($table = null) 83 | { 84 | global $db; 85 | return $db->clear($table); 86 | } 87 | function db_get_tables() 88 | { 89 | global $db; 90 | return $db->get_tables(); 91 | } 92 | function db_get_columns($table) 93 | { 94 | global $db; 95 | return $db->get_columns($table); 96 | } 97 | function db_get_foreign_keys($table) 98 | { 99 | global $db; 100 | return $db->get_foreign_keys($table); 101 | } 102 | function db_is_foreign_key($table, $column) 103 | { 104 | global $db; 105 | return $db->db_is_foreign_key($table, $column); 106 | } 107 | function db_has_table($table) 108 | { 109 | global $db; 110 | return $db->has_table($table); 111 | } 112 | function db_has_column($table, $column) 113 | { 114 | global $db; 115 | return $db->has_column($table, $column); 116 | } 117 | function db_get_datatype($table, $column) 118 | { 119 | global $db; 120 | return $db->get_datatype($table, $column); 121 | } 122 | function db_get_primary_key($table) 123 | { 124 | global $db; 125 | return $db->get_primary_key($table); 126 | } 127 | function db_uuid() 128 | { 129 | global $db; 130 | return $db->uuid(); 131 | } 132 | function db_setup_logging() 133 | { 134 | global $db; 135 | return $db->setup_logging(); 136 | } 137 | function db_disable_logging() 138 | { 139 | global $db; 140 | return $db->disable_logging(); 141 | } 142 | function db_enable_logging() 143 | { 144 | global $db; 145 | return $db->enable_logging(); 146 | } 147 | function db_enable_auto_inject() 148 | { 149 | global $db; 150 | return $db->enable_auto_inject(); 151 | } 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build status](https://github.com/vielhuber/dbhelper/actions/workflows/ci.yml/badge.svg)](https://github.com/vielhuber/dbhelper/actions) 2 | 3 | # 🍗 dbhelper 🍗 4 | 5 | dbhelper is a small php wrapper for mysql/postgres/sqlite databases. 6 | 7 | ## installation 8 | 9 | install once with composer: 10 | 11 | ``` 12 | composer require vielhuber/dbhelper 13 | ``` 14 | 15 | then add this to your project: 16 | 17 | ```php 18 | require __DIR__ . '/vendor/autoload.php'; 19 | use vielhuber\dbhelper\dbhelper; 20 | $db = new dbhelper(); 21 | ``` 22 | 23 | ## usage 24 | 25 | ```php 26 | /* connect to database */ 27 | $db->connect('pdo', 'mysql', '127.0.0.1', 'username', 'password', 'database', 3306); 28 | $db->connect('pdo', 'postgres', '127.0.0.1', 'username', 'password', 'database', 5432); 29 | $db->connect('pdo', 'sqlite', 'database.db'); 30 | $db->connect('pdo', 'sqlite', 'database.db', null, null, null, null, 120); // specify a manual timeout of 120 seconds 31 | $db->connect('pdo', 'mysql', '127.0.0.1', 'username', 'password', null, 3306); // database must not be available 32 | 33 | /* disconnect from database */ 34 | $db->disconnect(); 35 | 36 | /* insert/update/delete */ 37 | $id = $db->insert('tablename', ['col1' => 'foo']); 38 | $db->update('tablename', ['col1' => 'bar'], ['id' => $id]); 39 | $db->delete('tablename', ['id' => $id]); 40 | 41 | /* select */ 42 | $db->fetch_all('SELECT * FROM tablename WHERE name = ? AND number > ?', 'foo', 42); 43 | $db->fetch_row('SELECT * FROM tablename WHERE ID = ?', 1); 44 | $db->fetch_col('SELECT col FROM tablename WHERE ID > ?', 1); 45 | $db->fetch_var('SELECT col FROM tablename WHERE ID = ?', 1); 46 | 47 | /* count */ 48 | $db->count('tablename') // 42 49 | $db->count('tablename', ['col1' => 'foo']) // 7 50 | 51 | /* automatic flattened arguments */ 52 | $db->fetch_all('SELECT * FROM tablename WHERE ID = ?', [1], 2, [3], [4,[5,6]]); 53 | // gets transformed to 54 | $db->fetch_all('SELECT * FROM tablename WHERE ID = ?', 1, 2, 3, 4, 5, 6); 55 | 56 | /* automatic in-expansion */ 57 | $db->fetch_all('SELECT * FROM tablename WHERE col1 = ? AND col2 IN (?)', 1, [2,3,4]); 58 | 59 | /* support for null values */ 60 | $db->query('UPDATE tablename SET col1 = ? WHERE col2 = ? AND col3 != ?', null, null, null); 61 | // gets transformed to 62 | $db->query('UPDATE tablename SET col1 = NULL WHERE col2 IS NULL AND col3 IS NOT NULL'); 63 | 64 | /* clean up */ 65 | $db->clear(); // delete all tables (without dropping the whole database) 66 | $db->clear('tablename'); // delete all rows in a table 67 | 68 | /* delete table */ 69 | $db->delete_table('tablename'); 70 | 71 | /* create table */ 72 | $db->create_table('tablename', [ 73 | 'id' => 'SERIAL PRIMARY KEY', // use INTEGER instead of SERIAL on sqlite to get auto ids 74 | 'col1' => 'varchar(255)', 75 | 'col2' => 'varchar(255)', 76 | 'col3' => 'varchar(255)' 77 | ]); 78 | 79 | /* create if not exists and connect to database */ 80 | $db->connect_with_create('pdo', 'mysql', '127.0.0.1', 'username', 'password', 'database', 3306); 81 | // this is a shorthand for 82 | $db->connect('pdo', 'mysql', '127.0.0.1', 'username', 'password', null, 3306); 83 | $db->create_database('database'); 84 | $db->disconnect(); 85 | $db->connect('pdo', 'mysql', '127.0.0.1', 'username', 'password', 'database', 3306); 86 | 87 | /* delete database */ 88 | $db->disconnect_with_delete(); 89 | // this is a shorthand for 90 | $db->disconnect(); 91 | $db->connect('pdo', 'mysql', '127.0.0.1', 'username', 'password', null, 3306); 92 | $db->delete_database('database'); 93 | $db->disconnect(); 94 | 95 | /* raw queries */ 96 | $db->query('INSERT INTO tablename(row1, row2) VALUES(?, ?, ?)', 1, 2, 3); 97 | $db->query('UPDATE tablename SET row1 = ? WHERE ID = ?', 1, 2); 98 | $db->query('DELETE FROM tablename WHERE ID = ?', 1); 99 | 100 | /* quickly debug raw queries */ 101 | $db->debug('DELETE FROM tablename WHERE row1 = ?', null); // DELETE FROM tablename WHERE row1 IS NULL 102 | 103 | /* last insert id */ 104 | $db->insert('tablename', ['col1' => 'foo']); 105 | $db->last_insert_id(); 106 | 107 | /* some more little helpers */ 108 | $db->get_tables() // ['tablename', ...] 109 | $db->has_table('tablename') // true 110 | $db->get_columns('tablename') // ['col1', 'col2', ...] 111 | $db->has_column('tablename', 'col1') // true 112 | $db->get_datatype('tablename', 'col1') // varchar 113 | $db->get_primary_key('tablename') // id 114 | $db->uuid() // generate uuid (v4) from inside the database 115 | $db->get_foreign_keys('users') // [['address_id' => ['addresses','id'], ...] 116 | $db->is_foreign_key('users', 'address_id') // true 117 | $db->get_foreign_tables_out('users') // [['addresses' => [['address_id','id']], ...] 118 | $db->get_foreign_tables_in('addresses') // [['users' => [['address_id','id']], ...] 119 | 120 | /* handle duplicates */ 121 | $db->get_duplicates() // ['count' => ['tbl1' => 3, 'tbl2' => 17], 'data' => ['tbl1' => [...], 'tbl2' => [...]] 122 | $db->delete_duplicates('tablename') // delete duplicates based on all columns except the primary key 123 | $db->delete_duplicates('tablename', ['common_col1','common_col1','common_col1']) // based on specific columns 124 | $db->delete_duplicates('tablename', ['common_col1','common_col1','common_col1'], false) // null values are considered equal by default; you can disable this untypical behaviour for sql with "false" 125 | $db->delete_duplicates('tablename', ['common_col1','common_col1','common_col1'], true, ['id' => 'asc']) // keep row with lowest primary key "id" (normally this is 'id' => 'desc') 126 | $db->delete_duplicates('tablename', ['common_col1','common_col1','common_col1'], true, ['id' => 'asc'], false) // case insensitive match (normally this is case sensitive) 127 | 128 | /* globally trim values */ 129 | $db->trim_values() // [['table' => 'tbl1', 'column' => 'col1', 'id' => 1, 'before' => ' foo', 'after' => 'foo'], ...] 130 | $db->trim_values(false) // by default trim_values does a dry run (no updates) 131 | $db->trim_values(true) // do real updates 132 | $db->trim_values(false, ['table1', 'table2' => ['col1', 'col2']]) // ignore tables and columns 133 | 134 | /* batch functions (they create only one query) */ 135 | $db->insert('tablename', [ 136 | ['id' => 1, 'name' => 'foo1'], 137 | ['id' => 2, 'name' => 'foo2'], 138 | ['id' => 3, 'name' => 'foo3'] 139 | ]); 140 | $db->delete('tablename', [ 141 | ['id' => 1], 142 | ['id' => 7], 143 | ['id' => 42] 144 | ]); 145 | $db->update('tablename', [ 146 | [['col1' => 'var1', 'col2' => 1], ['id' => 1, 'key' => '1']], 147 | [['col1' => 'var2', 'col2' => 2], ['id' => 2, 'key' => '2']], 148 | [['col1' => 'var3', 'col2' => 3], ['id' => 3, 'key' => '3']] 149 | ]); 150 | /* 151 | this generates the following query: 152 | UPDATE tablename SET 153 | col1 = CASE WHEN (id = 1 AND key = '1') THEN 'var1' WHEN (id = 2 AND key = '2') THEN 'var2' WHEN (id = 3 AND key = '3') THEN 'var3' END, 154 | col2 = CASE WHEN (id = 1 AND key = '1') THEN 1 WHEN (id = 2 AND key = '2') THEN 2 WHEN (id = 3 AND key = '3') THEN 3 END 155 | WHERE id IN (1,2,3) AND key IN ('1','2','3'); 156 | */ 157 | ``` 158 | 159 | ### logging 160 | 161 | dbhelper can support setting up a mature logging system on mysql/postgres databases. 162 | 163 | ```php 164 | $db = new dbhelper([ 165 | 'logging_table' => 'logs', 166 | 'exclude' => [ 167 | 'tables' => ['table1'], 168 | 'columns' => ['table2' => ['col1', 'col2', 'col3']] 169 | ], 170 | 'delete_older' => 12, // months 171 | 'updated_by' => get_current_user_id() 172 | ]); 173 | $db->connect('...'); 174 | $db->setup_logging(); 175 | ``` 176 | 177 | `setup_logging()` does four things: 178 | 179 | - it creates a logging table (if not exists) 180 | - it appends a single column `updated_by` to every table in the database (if not exists) 181 | - it creates triggers for all insert/update/delete events (if not exists) 182 | - it deletes old logging entries based on the `delete_older` option 183 | 184 | you should run this method after a schema change (e.g. in your migrations) and you can also run it on a daily basis via cron. it is recommened to exclude blob/bytea columns. 185 | 186 | the logging table has the following schema: 187 | 188 | - `id`: unique identifier of that single change 189 | - `log_event`: insert/update/delete 190 | - `log_table`: name of the table of the modified row 191 | - `log_key`: key of the modified row 192 | - `log_column`: column of the modified row 193 | - `log_value`: value of the modified row 194 | - `log_uuid`: unique identifier of that row change 195 | - `updated_by`: who did make that change 196 | - `updated_at`: date and time of the event 197 | 198 | we now have to adjust our queries. `updated_by` must be populated by the web application on all insert/update queries and our logging table must be manually populated before delete queries: 199 | 200 | ```php 201 | $db->insert('tablename', ['col1' => 'foo', 'updated_by' => get_current_user_id()]); 202 | 203 | $db->update('tablename', ['col1' => 'foo', 'updated_by' => get_current_user_id()], ['id' => 42]); 204 | 205 | $db->insert('logs', [ 206 | 'log_event' => 'delete', 207 | 'log_table' => 'tablename', 208 | 'log_key' => 42, 209 | 'log_uuid' => $db->uuid(), 210 | 'updated_by' => get_current_user_id() 211 | ]); 212 | $db->delete('tablename', ['id' => 42]); 213 | ``` 214 | 215 | instead of all this we can let dbhelper magically do the heavy lifting on every insert/update/delete for us: 216 | 217 | ```php 218 | $db->enable_auto_inject(); 219 | ``` 220 | 221 | dbhelper then automatically injects the `updated_by` column on all insert/update statements and inserts a log entry before every delete query (all queries are handled, even those who are sent with `$db->query`). 222 | 223 | important note: if we manipulate data outside of our web application, the triggers also work, except with accurate values in `updated_by`. this is especially true for delete statements (they also work without the manual insert query upfront). 224 | 225 | call the following helper functions, if you (temporarily) need to disable logging by triggers: 226 | 227 | ```php 228 | $db->disable_logging(); 229 | $db->query('DELETE * FROM mega_big_table'); 230 | $db->enable_logging(); 231 | ``` 232 | 233 | that's it – happy logging. 234 | 235 | ### wordpress support 236 | 237 | this also works for wordpress (using wpdb, prepared statements and stripslashes_deep under the hood): 238 | 239 | ```php 240 | $db->connect('wordpress'); 241 | $db->fetch_var('SELECT col FROM tablename WHERE ID = ?', 1); 242 | ``` 243 | 244 | ### locking in sqlite 245 | 246 | sqlite is nice but database locking can be tricky.\ 247 | dbhelper provides a default timeout of `60` seconds, which prevents most database locks.\ 248 | you can manually define a timeout in the `connect()` function.\ 249 | checkout the following sqlite lock tests: 250 | 251 | - `php tests/lock/run.php 1`: runs into database locking 252 | - `php tests/lock/run.php 120`: does not run into database locking 253 | 254 | also consider enabling [wal](https://sqlite.org/wal.html) via `$db->query('PRAGMA journal_mode=WAL;');`. 255 | 256 | ### return values 257 | 258 | as return values after fetching results dbhelper usually returns associative arrays.\ 259 | if you use it with wordpress, objects are returned.\ 260 | dbhelper throws exceptions on all occured errors.\ 261 | on an `insert` operation, the primary key (id) is returned.\ 262 | on any `delete`, `update` or even `query` operation, the number of affected rows are returned. 263 | 264 | ### static version 265 | 266 | here is also a static version with static function calls (this makes sense, if you use a single instance of dbhelper): 267 | 268 | ```php 269 | $db = new dbhelper(); 270 | require_once $_SERVER['DOCUMENT_ROOT'] . '/vendor/vielhuber/dbhelper/src/static.php'; 271 | db_connect('pdo', 'mysql', '127.0.0.1', 'username', 'password', 'database', 3306); 272 | db_fetch_var('SELECT col FROM tablename WHERE ID = ?', 1); 273 | ``` 274 | -------------------------------------------------------------------------------- /tests/phpunit/LogTest.php: -------------------------------------------------------------------------------- 1 | insert('test', ['col1' => 'foo1']); 13 | $this->assertEquals(self::$db->fetch_row('SELECT * FROM test WHERE id = ?', $id), [ 14 | 'id' => $id, 15 | 'col1' => 'foo1', 16 | 'col2' => null, 17 | 'col3' => null, 18 | 'col4' => null, 19 | 'updated_by' => 42 20 | ]); 21 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 22 | unset($row['id']); 23 | unset($row['log_key']); 24 | unset($row['log_uuid']); 25 | unset($row['updated_at']); 26 | $this->assertEquals($row, [ 27 | 'log_event' => 'insert', 28 | 'log_table' => 'test', 29 | 'log_column' => 'col3', 30 | 'log_value' => null, 31 | 'updated_by' => 42 32 | ]); 33 | 34 | $id = self::$db->insert('test', ['col1' => 'foo2', 'updated_by' => 43]); 35 | $this->assertEquals(self::$db->fetch_row('SELECT * FROM test WHERE id = ?', $id), [ 36 | 'id' => $id, 37 | 'col1' => 'foo2', 38 | 'col2' => null, 39 | 'col3' => null, 40 | 'col4' => null, 41 | 'updated_by' => 43 42 | ]); 43 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 44 | unset($row['id']); 45 | unset($row['log_key']); 46 | unset($row['log_uuid']); 47 | unset($row['updated_at']); 48 | $this->assertEquals($row, [ 49 | 'log_event' => 'insert', 50 | 'log_table' => 'test', 51 | 'log_column' => 'col3', 52 | 'log_value' => null, 53 | 'updated_by' => 43 54 | ]); 55 | 56 | self::$db->query('INSERT INTO test(col1, col2, col3) VALUES(?,?,?)', ['foo3', 'foo3', 3]); 57 | $this->assertEquals(self::$db->fetch_row('SELECT * FROM test ORDER BY id DESC LIMIT 1'), [ 58 | 'id' => ++$id, 59 | 'col1' => 'foo3', 60 | 'col2' => 'foo3', 61 | 'col3' => 3, 62 | 'col4' => null, 63 | 'updated_by' => 42 64 | ]); 65 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 66 | unset($row['id']); 67 | unset($row['log_key']); 68 | unset($row['log_uuid']); 69 | unset($row['updated_at']); 70 | $this->assertEquals($row, [ 71 | 'log_event' => 'insert', 72 | 'log_table' => 'test', 73 | 'log_column' => 'col3', 74 | 'log_value' => 3, 75 | 'updated_by' => 42 76 | ]); 77 | 78 | self::$db->query( 79 | ' 80 | insert into 81 | test (col1, col2, col3) VALUES (?, ?, ?) 82 | ', 83 | ['foo4', 'foo4', 4] 84 | ); 85 | $this->assertEquals(self::$db->fetch_row('SELECT * FROM test ORDER BY id DESC LIMIT 1'), [ 86 | 'id' => ++$id, 87 | 'col1' => 'foo4', 88 | 'col2' => 'foo4', 89 | 'col3' => 4, 90 | 'col4' => null, 91 | 'updated_by' => 42 92 | ]); 93 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 94 | unset($row['id']); 95 | unset($row['log_key']); 96 | unset($row['log_uuid']); 97 | unset($row['updated_at']); 98 | $this->assertEquals($row, [ 99 | 'log_event' => 'insert', 100 | 'log_table' => 'test', 101 | 'log_column' => 'col3', 102 | 'log_value' => 4, 103 | 'updated_by' => 42 104 | ]); 105 | 106 | $id = self::$db->insert('test2', ['col1' => 'foo1']); 107 | $this->assertEquals(self::$db->fetch_row('SELECT * FROM test2 WHERE id = ?', $id), [ 108 | 'id' => $id, 109 | 'col1' => 'foo1', 110 | 'col2' => null, 111 | 'col3' => null, 112 | 'col4' => null 113 | ]); 114 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 115 | unset($row['id']); 116 | unset($row['log_key']); 117 | unset($row['log_uuid']); 118 | unset($row['updated_at']); 119 | $this->assertEquals($row, [ 120 | 'log_event' => 'insert', 121 | 'log_table' => 'test', 122 | 'log_column' => 'col3', 123 | 'log_value' => 4, 124 | 'updated_by' => 42 125 | ]); 126 | 127 | $id = self::$db->insert('test', ['col1' => 'foo1']); 128 | $this->assertEquals(self::$db->fetch_row('SELECT * FROM test WHERE id = ?', $id), [ 129 | 'id' => $id, 130 | 'col1' => 'foo1', 131 | 'col2' => null, 132 | 'col3' => null, 133 | 'col4' => null, 134 | 'updated_by' => 42 135 | ]); 136 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 137 | unset($row['id']); 138 | unset($row['log_key']); 139 | unset($row['log_uuid']); 140 | unset($row['updated_at']); 141 | $this->assertEquals($row, [ 142 | 'log_event' => 'insert', 143 | 'log_table' => 'test', 144 | 'log_column' => 'col3', 145 | 'log_value' => null, 146 | 'updated_by' => 42 147 | ]); 148 | 149 | $id = self::$db->insert('test', ['col2' => str_repeat('x', 5000)]); 150 | $this->assertEquals(self::$db->fetch_var('SELECT SUBSTRING(col2, 1, 3) FROM test WHERE id = ?', $id), 'xxx'); 151 | $this->assertEquals( 152 | self::$db->fetch_var( 153 | 'SELECT SUBSTRING(log_value, 1, 3) FROM logs WHERE log_column = ? ORDER BY id DESC LIMIT 1', 154 | 'col2' 155 | ), 156 | 'xxx' 157 | ); 158 | } 159 | 160 | function test__update() 161 | { 162 | $id = self::$db->insert('test', ['col1' => 'foo', 'updated_by' => 43]); 163 | 164 | self::$db->update('test', ['col1' => 'bar'], ['id' => $id]); 165 | $this->assertEquals(self::$db->fetch_var('SELECT updated_by FROM test ORDER BY id DESC LIMIT 1'), 42); 166 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 167 | unset($row['id']); 168 | unset($row['log_key']); 169 | unset($row['log_uuid']); 170 | unset($row['updated_at']); 171 | $this->assertEquals($row, [ 172 | 'log_event' => 'update', 173 | 'log_table' => 'test', 174 | 'log_column' => 'col1', 175 | 'log_value' => 'bar', 176 | 'updated_by' => 42 177 | ]); 178 | 179 | self::$db->update('test', ['col1' => 'foo', 'updated_by' => 43], ['id' => $id]); 180 | $this->assertEquals(self::$db->fetch_var('SELECT updated_by FROM test ORDER BY id DESC LIMIT 1'), 43); 181 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 182 | unset($row['id']); 183 | unset($row['log_key']); 184 | unset($row['log_uuid']); 185 | unset($row['updated_at']); 186 | $this->assertEquals($row, [ 187 | 'log_event' => 'update', 188 | 'log_table' => 'test', 189 | 'log_column' => 'col1', 190 | 'log_value' => 'foo', 191 | 'updated_by' => 43 192 | ]); 193 | 194 | self::$db->query('UPDATE test SET col1 = ?, col2 = ?, col3 = ?', ['foo3', 'foo3', 3]); 195 | $this->assertEquals(self::$db->fetch_row('SELECT * FROM test ORDER BY id DESC LIMIT 1'), [ 196 | 'id' => $id, 197 | 'col1' => 'foo3', 198 | 'col2' => 'foo3', 199 | 'col3' => 3, 200 | 'col4' => null, 201 | 'updated_by' => 42 202 | ]); 203 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 204 | unset($row['id']); 205 | unset($row['log_key']); 206 | unset($row['log_uuid']); 207 | unset($row['updated_at']); 208 | $this->assertEquals($row, [ 209 | 'log_event' => 'update', 210 | 'log_table' => 'test', 211 | 'log_column' => 'col3', 212 | 'log_value' => 3, 213 | 'updated_by' => 42 214 | ]); 215 | } 216 | 217 | function test__delete() 218 | { 219 | $id = self::$db->insert('test', ['col1' => 'lorem1']); 220 | self::$db->insert('test', ['col1' => 'lorem2']); 221 | self::$db->insert('test', ['col1' => 'lorem3']); 222 | self::$db->insert('test', ['col1' => 'lorem4']); 223 | self::$db->insert('test', ['col1' => 'lorem5']); 224 | self::$db->insert('test', ['col1' => 'ipsum1']); 225 | 226 | self::$db->delete('test', ['col1' => 'lorem1']); 227 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 228 | unset($row['id']); 229 | unset($row['log_uuid']); 230 | unset($row['updated_at']); 231 | $this->assertEquals($row, [ 232 | 'log_event' => 'delete', 233 | 'log_table' => 'test', 234 | 'log_key' => $id, 235 | 'log_column' => null, 236 | 'log_value' => null, 237 | 'updated_by' => 42 238 | ]); 239 | 240 | self::$db->query('DELETE FROM test WHERE col1 IN (?)', ['lorem2', 'lorem3']); 241 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 242 | unset($row['id']); 243 | unset($row['log_uuid']); 244 | unset($row['updated_at']); 245 | $this->assertEquals($row, [ 246 | 'log_event' => 'delete', 247 | 'log_table' => 'test', 248 | 'log_key' => $id + 2, 249 | 'log_column' => null, 250 | 'log_value' => null, 251 | 'updated_by' => 42 252 | ]); 253 | 254 | self::$db->query(' delete from test WHERE col1 LIKE ? ', '%lorem%'); 255 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 256 | unset($row['id']); 257 | unset($row['log_uuid']); 258 | unset($row['updated_at']); 259 | $this->assertEquals($row, [ 260 | 'log_event' => 'delete', 261 | 'log_table' => 'test', 262 | 'log_key' => $id + 4, 263 | 'log_column' => null, 264 | 'log_value' => null, 265 | 'updated_by' => 42 266 | ]); 267 | 268 | self::$db->query('DELETE FROM test'); 269 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 270 | unset($row['id']); 271 | unset($row['log_uuid']); 272 | unset($row['updated_at']); 273 | $this->assertEquals($row, [ 274 | 'log_event' => 'delete', 275 | 'log_table' => 'test', 276 | 'log_key' => $id + 5, 277 | 'log_column' => null, 278 | 'log_value' => null, 279 | 'updated_by' => 42 280 | ]); 281 | } 282 | 283 | function test__enable_disable() 284 | { 285 | $id = self::$db->insert('test', ['col3' => 9991]); 286 | $this->assertEquals(self::$db->fetch_row('SELECT * FROM test WHERE id = ?', $id), [ 287 | 'id' => $id, 288 | 'col1' => null, 289 | 'col2' => null, 290 | 'col3' => 9991, 291 | 'col4' => null, 292 | 'updated_by' => 42 293 | ]); 294 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 295 | unset($row['id']); 296 | unset($row['log_key']); 297 | unset($row['log_uuid']); 298 | unset($row['updated_at']); 299 | $this->assertEquals($row, [ 300 | 'log_event' => 'insert', 301 | 'log_table' => 'test', 302 | 'log_column' => 'col3', 303 | 'log_value' => 9991, 304 | 'updated_by' => 42 305 | ]); 306 | self::$db->disable_logging(); 307 | $id = self::$db->insert('test', ['col3' => 9992]); 308 | $this->assertEquals(self::$db->fetch_row('SELECT * FROM test WHERE id = ?', $id), [ 309 | 'id' => $id, 310 | 'col1' => null, 311 | 'col2' => null, 312 | 'col3' => 9992, 313 | 'col4' => null, 314 | 'updated_by' => 42 315 | ]); 316 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 317 | unset($row['id']); 318 | unset($row['log_key']); 319 | unset($row['log_uuid']); 320 | unset($row['updated_at']); 321 | $this->assertEquals($row, [ 322 | 'log_event' => 'insert', 323 | 'log_table' => 'test', 324 | 'log_column' => 'col3', 325 | 'log_value' => 9991, 326 | 'updated_by' => 42 327 | ]); 328 | self::$db->enable_logging(); 329 | $id = self::$db->insert('test', ['col3' => 9993]); 330 | $this->assertEquals(self::$db->fetch_row('SELECT * FROM test WHERE id = ?', $id), [ 331 | 'id' => $id, 332 | 'col1' => null, 333 | 'col2' => null, 334 | 'col3' => 9993, 335 | 'col4' => null, 336 | 'updated_by' => 42 337 | ]); 338 | $row = self::$db->fetch_row('SELECT * FROM logs ORDER BY id DESC LIMIT 1'); 339 | unset($row['id']); 340 | unset($row['log_key']); 341 | unset($row['log_uuid']); 342 | unset($row['updated_at']); 343 | $this->assertEquals($row, [ 344 | 'log_event' => 'insert', 345 | 'log_table' => 'test', 346 | 'log_column' => 'col3', 347 | 'log_value' => 9993, 348 | 'updated_by' => 42 349 | ]); 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /tests/phpunit/BasicTest.php: -------------------------------------------------------------------------------- 1 | insert('test', ['col1' => 'foo']); 15 | $this->assertEquals(self::$db->fetch_var('SELECT col1 FROM test WHERE id = ?', $id), 'foo'); 16 | $id = self::$db->insert('test', [ 17 | 'id' => 2, 18 | 'col1' => 'foo', 19 | 'col2' => 'bar', 20 | 'col3' => null 21 | ]); 22 | $this->assertEquals(self::$db->fetch_var('SELECT col3 FROM test WHERE id = ?', $id), null); 23 | } 24 | 25 | function test__update() 26 | { 27 | $id = self::$db->insert('test', ['col1' => 'foo']); 28 | self::$db->update('test', ['col1' => 'bar'], ['col1' => 'foo']); 29 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test WHERE col1 = ?', 'foo'), 0); 30 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test WHERE col1 = ?', 'bar'), 1); 31 | $this->assertEquals(self::$db->update('test', ['col1' => 'foo'], ['col1' => 'bar']), 1); 32 | $this->assertEquals(self::$db->update('test', ['col1' => 'foo'], ['col1' => 'bar']), 0); 33 | } 34 | 35 | function test__delete() 36 | { 37 | $id = self::$db->insert('test', ['col1' => 'foo']); 38 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test WHERE col1 = ?', 'foo'), 1); 39 | $id = self::$db->insert('test', ['col1' => 'bar']); 40 | $this->assertEquals( 41 | self::$db->fetch_var('SELECT COUNT(*) FROM test WHERE col1 = ? OR col1 = ?', 'foo', 'bar'), 42 | 2 43 | ); 44 | $this->assertEquals(self::$db->delete('test', ['col1' => 'foo']), 1); 45 | $this->assertEquals( 46 | self::$db->fetch_var('SELECT COUNT(*) FROM test WHERE col1 = ? OR col1 = ?', 'foo', 'bar'), 47 | 1 48 | ); 49 | $this->assertEquals(self::$db->query('DELETE FROM test WHERE col1 = ?', 'bar'), 1); 50 | $this->assertEquals( 51 | self::$db->fetch_var('SELECT COUNT(*) FROM test WHERE col1 = ? OR col1 = ?', 'foo', 'bar'), 52 | 0 53 | ); 54 | } 55 | 56 | function test__fetch_all() 57 | { 58 | $id1 = self::$db->insert('test', ['col1' => 'foo']); 59 | $id2 = self::$db->insert('test', ['col1' => 'bar']); 60 | $this->assertEquals(self::$db->fetch_all('SELECT * FROM test WHERE col1 = ?', 'foo'), [ 61 | ['id' => $id1, 'col1' => 'foo', 'col2' => null, 'col3' => null] 62 | ]); 63 | $this->assertEquals(self::$db->fetch_all('SELECT * FROM test WHERE col1 = ? OR col1 = ?', 'foo', 'bar'), [ 64 | ['id' => $id1, 'col1' => 'foo', 'col2' => null, 'col3' => null], 65 | ['id' => $id2, 'col1' => 'bar', 'col2' => null, 'col3' => null] 66 | ]); 67 | } 68 | 69 | function test__fetch_row() 70 | { 71 | $id = self::$db->insert('test', ['col1' => 'foo']); 72 | $this->assertEquals(self::$db->fetch_row('SELECT * FROM test WHERE id = ?', $id), [ 73 | 'id' => $id, 74 | 'col1' => 'foo', 75 | 'col2' => null, 76 | 'col3' => null 77 | ]); 78 | } 79 | 80 | function test__fetch_col() 81 | { 82 | self::$db->insert('test', ['col1' => 'foo']); 83 | self::$db->insert('test', ['col1' => 'bar']); 84 | $this->assertEquals(self::$db->fetch_col('SELECT col1 FROM test'), ['foo', 'bar']); 85 | } 86 | 87 | function test__fetch_var() 88 | { 89 | $id1 = self::$db->insert('test', ['col1' => 'foo']); 90 | $id2 = self::$db->insert('test', ['col1' => 'bar']); 91 | $this->assertEquals(self::$db->fetch_var('SELECT col1 FROM test WHERE id = ?', $id1), 'foo'); 92 | $this->assertEquals(self::$db->fetch_var('SELECT col1 FROM test WHERE id = ?', $id2), 'bar'); 93 | } 94 | 95 | function test__count() 96 | { 97 | $this->assertEquals(self::$db->count('test'), 0); 98 | $this->assertEquals(self::$db->count('test', ['id' => 1]), 0); 99 | self::$db->insert('test', ['col1' => 'foo']); 100 | $this->assertEquals(self::$db->count('test'), 1); 101 | $this->assertEquals(self::$db->count('test', ['id' => 1]), 1); 102 | self::$db->insert('test', ['col1' => 'bar']); 103 | $this->assertEquals(self::$db->count('test'), 2); 104 | $this->assertEquals(self::$db->count('test', ['id' => 1]), 1); 105 | self::$db->delete('test', ['id' => 2]); 106 | $this->assertEquals(self::$db->count('test'), 1); 107 | $this->assertEquals(self::$db->count('test', ['id' => 1]), 1); 108 | self::$db->delete('test', ['id' => 1]); 109 | $this->assertEquals(self::$db->count('test', ['id' => 1]), 0); 110 | $this->assertEquals(self::$db->count('test'), 0); 111 | } 112 | 113 | function test__flattened_args() 114 | { 115 | $id = self::$db->insert('test', ['col1' => 'foo', 'col2' => 'bar', 'col3' => 'baz']); 116 | $this->assertEquals( 117 | self::$db->fetch_row('SELECT * FROM test WHERE col1 = ? AND col2 = ? AND col3 = ?', 'foo', 'bar', 'baz'), 118 | ['id' => $id, 'col1' => 'foo', 'col2' => 'bar', 'col3' => 'baz'] 119 | ); 120 | $this->assertEquals( 121 | self::$db->fetch_row( 122 | 'SELECT * FROM test WHERE col1 = ? AND col2 = ? AND col3 = ?', 123 | ['foo'], 124 | ['bar'], 125 | [[[['baz']]]] 126 | ), 127 | ['id' => $id, 'col1' => 'foo', 'col2' => 'bar', 'col3' => 'baz'] 128 | ); 129 | $this->assertEquals( 130 | self::$db->fetch_row('SELECT * FROM test WHERE col1 = ? AND col2 = ? AND col3 = ?', ['foo', 'bar'], 'baz'), 131 | ['id' => $id, 'col1' => 'foo', 'col2' => 'bar', 'col3' => 'baz'] 132 | ); 133 | $this->assertEquals( 134 | self::$db->fetch_row('SELECT * FROM test WHERE col1 = ? AND col2 = ? AND col3 = ?', 'foo', [ 135 | 'bar', 136 | ['baz'] 137 | ]), 138 | ['id' => $id, 'col1' => 'foo', 'col2' => 'bar', 'col3' => 'baz'] 139 | ); 140 | $this->assertEquals( 141 | self::$db->fetch_row('SELECT * FROM test WHERE col1 = ? AND col2 = ? AND col3 = ?', ['foo', 'bar', 'baz']), 142 | ['id' => $id, 'col1' => 'foo', 'col2' => 'bar', 'col3' => 'baz'] 143 | ); 144 | } 145 | 146 | function test__in_expansion() 147 | { 148 | self::$db->insert('test', [ 149 | ['id' => 1, 'col1' => 'foo', 'col2' => 'bar', 'col3' => 'baz'], 150 | ['id' => 2, 'col1' => 'foo', 'col2' => 'baz', 'col3' => 'foo'], 151 | ['id' => 3, 'col1' => 'foo', 'col2' => 'foo', 'col3' => 'bar'] 152 | ]); 153 | $this->assertEquals( 154 | self::$db->fetch_all('SELECT * FROM test WHERE col1 = ? AND col2 IN (?)', 'foo', ['bar', 'baz']), 155 | [ 156 | ['id' => 1, 'col1' => 'foo', 'col2' => 'bar', 'col3' => 'baz'], 157 | ['id' => 2, 'col1' => 'foo', 'col2' => 'baz', 'col3' => 'foo'] 158 | ] 159 | ); 160 | $this->assertEquals( 161 | self::$db->fetch_all('SELECT * FROM test WHERE col1 = ? AND col2 NOT IN (?)', 'foo', ['bar', 'baz']), 162 | [['id' => 3, 'col1' => 'foo', 'col2' => 'foo', 'col3' => 'bar']] 163 | ); 164 | $this->assertEquals(self::$db->fetch_all('SELECT * FROM test WHERE col1 IN (?)', ['foo', 'bar', 'baz']), [ 165 | ['id' => 1, 'col1' => 'foo', 'col2' => 'bar', 'col3' => 'baz'], 166 | ['id' => 2, 'col1' => 'foo', 'col2' => 'baz', 'col3' => 'foo'], 167 | ['id' => 3, 'col1' => 'foo', 'col2' => 'foo', 'col3' => 'bar'] 168 | ]); 169 | $this->assertEquals( 170 | self::$db->fetch_all( 171 | 'SELECT * FROM test WHERE col1 IN (?) OR col2 IN (?) OR col3 IN (?)', 172 | 'foo', 173 | 'bar', 174 | 'baz' 175 | ), 176 | [ 177 | ['id' => 1, 'col1' => 'foo', 'col2' => 'bar', 'col3' => 'baz'], 178 | ['id' => 2, 'col1' => 'foo', 'col2' => 'baz', 'col3' => 'foo'], 179 | ['id' => 3, 'col1' => 'foo', 'col2' => 'foo', 'col3' => 'bar'] 180 | ] 181 | ); 182 | $this->assertEquals( 183 | self::$db->fetch_all('SELECT * FROM test WHERE col1 IN (?) OR col2 = ? OR col3 = ?', ['foo'], 'bar', 'baz'), 184 | [ 185 | ['id' => 1, 'col1' => 'foo', 'col2' => 'bar', 'col3' => 'baz'], 186 | ['id' => 2, 'col1' => 'foo', 'col2' => 'baz', 'col3' => 'foo'], 187 | ['id' => 3, 'col1' => 'foo', 'col2' => 'foo', 'col3' => 'bar'] 188 | ] 189 | ); 190 | $this->assertEquals( 191 | self::$db->fetch_all('SELECT * FROM test WHERE col1 IN (?) OR col2 = ? OR col3 = ?', ['foo', 'bar', 'baz']), 192 | [ 193 | ['id' => 1, 'col1' => 'foo', 'col2' => 'bar', 'col3' => 'baz'], 194 | ['id' => 2, 'col1' => 'foo', 'col2' => 'baz', 'col3' => 'foo'], 195 | ['id' => 3, 'col1' => 'foo', 'col2' => 'foo', 'col3' => 'bar'] 196 | ] 197 | ); 198 | $this->assertEquals( 199 | self::$db->fetch_all('SELECT * FROM test WHERE col1 IN (?,?) OR col2 = ?', ['foo', 'bar', 'baz']), 200 | [ 201 | ['id' => 1, 'col1' => 'foo', 'col2' => 'bar', 'col3' => 'baz'], 202 | ['id' => 2, 'col1' => 'foo', 'col2' => 'baz', 'col3' => 'foo'], 203 | ['id' => 3, 'col1' => 'foo', 'col2' => 'foo', 'col3' => 'bar'] 204 | ] 205 | ); 206 | } 207 | 208 | function test__null_values() 209 | { 210 | $id = self::$db->insert('test', ['col1' => 'foo', 'col2' => null, 'col3' => 'bar']); 211 | self::$db->query('UPDATE test SET col1 = NULL WHERE col2 IS NULL AND col3 IS NOT NULL'); 212 | $this->assertEquals(self::$db->fetch_row('SELECT * FROM test WHERE id = ?', $id), [ 213 | 'id' => $id, 214 | 'col1' => null, 215 | 'col2' => null, 216 | 'col3' => 'bar' 217 | ]); 218 | $id = self::$db->insert('test', ['col1' => 'foo', 'col2' => null, 'col3' => 'bar']); 219 | self::$db->query('UPDATE test SET col1 = ? WHERE col2 = ? AND col3 != ?', null, null, null); 220 | $this->assertEquals(self::$db->fetch_row('SELECT * FROM test WHERE id = ?', $id), [ 221 | 'id' => $id, 222 | 'col1' => null, 223 | 'col2' => null, 224 | 'col3' => 'bar' 225 | ]); 226 | } 227 | 228 | function test__batch() 229 | { 230 | self::$db->insert('test', [ 231 | ['id' => 1, 'col1' => 'foo'], 232 | ['id' => 2, 'col1' => 'bar'], 233 | ['id' => 3, 'col1' => 'baz'] 234 | ]); 235 | $this->assertEquals(self::$db->fetch_all('SELECT * FROM test'), [ 236 | ['id' => 1, 'col1' => 'foo', 'col2' => null, 'col3' => null], 237 | ['id' => 2, 'col1' => 'bar', 'col2' => null, 'col3' => null], 238 | ['id' => 3, 'col1' => 'baz', 'col2' => null, 'col3' => null] 239 | ]); 240 | self::$db->update('test', [ 241 | [['col1' => 'foo1'], ['id' => 1]], 242 | [['col1' => 'bar1'], ['id' => 2]], 243 | [['col1' => 'baz1'], ['id' => 3]] 244 | ]); 245 | $this->assertEquals(self::$db->fetch_all('SELECT * FROM test'), [ 246 | ['id' => 1, 'col1' => 'foo1', 'col2' => null, 'col3' => null], 247 | ['id' => 2, 'col1' => 'bar1', 'col2' => null, 'col3' => null], 248 | ['id' => 3, 'col1' => 'baz1', 'col2' => null, 'col3' => null] 249 | ]); 250 | self::$db->delete('test', [['id' => 1], ['id' => 2], ['id' => 3]]); 251 | $this->assertEquals(self::$db->fetch_all('SELECT * FROM test'), []); 252 | } 253 | 254 | function test__clear() 255 | { 256 | self::$db->insert('test', ['col1' => 'foo']); 257 | $this->assertEquals(1, self::$db->fetch_var('SELECT COUNT(*) FROM test')); 258 | self::$db->insert('test', ['col1' => 'foo']); 259 | self::$db->insert('test', ['col1' => 'foo']); 260 | self::$db->insert('test', ['col1' => 'foo']); 261 | $this->assertEquals(4, self::$db->fetch_var('SELECT COUNT(*) FROM test')); 262 | self::$db->clear('test'); 263 | $this->assertEquals(0, self::$db->fetch_var('SELECT COUNT(*) FROM test')); 264 | self::$db->clear(); 265 | try { 266 | self::$db->fetch_var('SELECT COUNT(*) FROM test'); 267 | $this->assertEquals(true, false); 268 | } catch (\Exception $e) { 269 | $this->assertEquals(true, true); 270 | } 271 | } 272 | 273 | function test__delete_table() 274 | { 275 | self::$db->insert('test', ['col1' => 'foo']); 276 | $this->assertEquals(1, self::$db->fetch_var('SELECT COUNT(*) FROM test')); 277 | self::$db->delete_table('test'); 278 | try { 279 | self::$db->fetch_var('SELECT COUNT(*) FROM test'); 280 | $this->assertEquals(true, false); 281 | } catch (\Exception $e) { 282 | $this->assertEquals(true, true); 283 | } 284 | } 285 | 286 | function test__create_table() 287 | { 288 | self::$db->create_table('test2', [ 289 | 'id' => 'SERIAL PRIMARY KEY', 290 | 'col1' => 'varchar(255)', 291 | 'col2' => 'varchar(255)', 292 | 'col3' => 'varchar(255)' 293 | ]); 294 | self::$db->insert('test', ['col1' => 'foo']); 295 | $this->assertEquals(1, self::$db->fetch_var('SELECT COUNT(*) FROM test')); 296 | self::$db->insert('test2', ['col1' => 'foo']); 297 | $this->assertEquals(1, self::$db->fetch_var('SELECT COUNT(*) FROM test2')); 298 | } 299 | 300 | function test__last_insert_id() 301 | { 302 | $id = self::$db->insert('test', ['col1' => 'foo']); 303 | $this->assertEquals($id, self::$db->last_insert_id()); 304 | } 305 | 306 | function test__insert_with_uuid() 307 | { 308 | self::$db->create_table('test_uuid', [ 309 | 'id' => 'varchar(36) PRIMARY KEY', 310 | 'col1' => 'varchar(255)' 311 | ]); 312 | $uuid = 'f81d4fae-7dec-11d0-a765-00a0c91e6bf6'; 313 | $id = self::$db->insert('test_uuid', ['id' => $uuid, 'col1' => 'foo']); 314 | $this->assertEquals($id, $uuid); 315 | } 316 | 317 | function test__get_tables() 318 | { 319 | $this->assertEquals(self::$db->get_tables(), ['test']); 320 | } 321 | 322 | function test__get_columns() 323 | { 324 | $this->assertEquals(self::$db->get_columns('test'), ['id', 'col1', 'col2', 'col3']); 325 | } 326 | 327 | function test__get_foreign_keys() 328 | { 329 | self::$db->create_table('test_foreign', [ 330 | 'id' => (self::$credentials->engine === 'sqlite' ? 'INTEGER' : 'SERIAL') . ' PRIMARY KEY', 331 | 'col1' => 'varchar(255)', 332 | 'col2' => 'varchar(255)', 333 | 'col3' => ['mysql' => 'BIGINT UNSIGNED', 'postgres' => 'INTEGER', 'sqlite' => ''][ 334 | self::$credentials->engine 335 | ], 336 | 'FOREIGN KEY(col3)' => 'REFERENCES test(id)' 337 | ]); 338 | $this->assertEquals(self::$db->get_foreign_keys('test'), []); 339 | $this->assertEquals(self::$db->is_foreign_key('test', 'col1'), false); 340 | $this->assertEquals(self::$db->get_foreign_keys('test_foreign'), ['col3' => ['test', 'id']]); 341 | $this->assertEquals(self::$db->is_foreign_key('test_foreign', 'col3'), true); 342 | 343 | $this->assertEquals(self::$db->get_foreign_tables_out('test'), []); 344 | $this->assertEquals(self::$db->get_foreign_tables_out('test_foreign'), ['test' => [['col3', 'id']]]); 345 | $this->assertEquals(self::$db->get_foreign_tables_in('test_foreign'), []); 346 | $this->assertEquals(self::$db->get_foreign_tables_in('test'), ['test_foreign' => [['col3', 'id']]]); 347 | } 348 | 349 | function test__get_duplicates() 350 | { 351 | self::$db->insert('test', ['col1' => 'foo1', 'col2' => 'bar1', 'col3' => 'baz1']); 352 | self::$db->insert('test', ['col1' => 'foo2', 'col2' => 'bar2', 'col3' => 'baz2']); 353 | $this->assertEquals(self::$db->get_duplicates('test'), ['count' => [], 'data' => []]); 354 | self::$db->insert('test', ['col1' => 'foo2', 'col2' => 'bar2', 'col3' => 'baz2']); 355 | $this->assertEquals(self::$db->get_duplicates('test'), [ 356 | 'count' => ['test' => 2], 357 | 'data' => ['test' => [['col1' => 'foo2', 'col2' => 'bar2', 'col3' => 'baz2', 'MIN()' => 2, 'COUNT()' => 2]]] 358 | ]); 359 | self::$db->insert('test', ['col1' => 'foo2', 'col2' => 'bar2', 'col3' => 'baz2']); 360 | self::$db->insert('test', ['col1' => 'foo2', 'col2' => 'bar2', 'col3' => 'baz2']); 361 | $this->assertEquals(self::$db->get_duplicates('test'), [ 362 | 'count' => ['test' => 4], 363 | 'data' => ['test' => [['col1' => 'foo2', 'col2' => 'bar2', 'col3' => 'baz2', 'MIN()' => 2, 'COUNT()' => 4]]] 364 | ]); 365 | self::$db->insert('test', ['col1' => 'foo1', 'col2' => 'bar1', 'col3' => 'baz1']); 366 | $this->assertEquals(self::$db->get_duplicates('test'), [ 367 | 'count' => ['test' => 6], 368 | 'data' => [ 369 | 'test' => [ 370 | ['col1' => 'foo1', 'col2' => 'bar1', 'col3' => 'baz1', 'MIN()' => 1, 'COUNT()' => 2], 371 | ['col1' => 'foo2', 'col2' => 'bar2', 'col3' => 'baz2', 'MIN()' => 2, 'COUNT()' => 4] 372 | ] 373 | ] 374 | ]); 375 | } 376 | 377 | function test__delete_duplicates() 378 | { 379 | self::$db->insert('test', ['col1' => 'foo1', 'col2' => 'bar1', 'col3' => 'baz1']); 380 | self::$db->insert('test', ['col1' => 'foo2', 'col2' => 'bar2', 'col3' => 'baz2']); 381 | self::$db->delete_duplicates('test'); 382 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test'), 2); 383 | self::$db->insert('test', ['col1' => 'foo1', 'col2' => 'bar1', 'col3' => 'baz1']); 384 | self::$db->insert('test', ['col1' => 'foo2', 'col2' => 'bar2', 'col3' => 'baz2']); 385 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test'), 4); 386 | self::$db->delete_duplicates('test'); 387 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test'), 2); 388 | self::$db->insert('test', ['col1' => null, 'col2' => 'bar3', 'col3' => 'baz2']); 389 | self::$db->insert('test', ['col1' => null, 'col2' => 'bar4', 'col3' => 'baz2']); 390 | self::$db->delete_duplicates('test', ['col2']); 391 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test'), 4); 392 | self::$db->delete_duplicates('test', ['col1', 'col3']); 393 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test'), 3); 394 | self::$db->clear('test'); 395 | 396 | self::$db->insert('test', ['col1' => null, 'col2' => 'bar1', 'col3' => 'baz1']); 397 | self::$db->insert('test', ['col1' => null, 'col2' => 'bar1', 'col3' => 'baz1']); 398 | self::$db->delete_duplicates('test', ['col1'], false); 399 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test'), 2); 400 | self::$db->delete_duplicates('test', ['col1'], true); 401 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test'), 1); 402 | self::$db->clear('test'); 403 | 404 | self::$db->insert('test', ['col1' => 'foo1', 'col2' => 'bar1', 'col3' => 'baz1']); 405 | self::$db->insert('test', ['col1' => 'foo1', 'col2' => 'bar1', 'col3' => 'baz1']); 406 | self::$db->delete_duplicates('test', ['col1'], true, ['id' => 'desc']); 407 | $this->assertEquals(self::$db->fetch_var('SELECT id FROM test LIMIT 1'), 2); 408 | self::$db->clear('test'); 409 | 410 | self::$db->insert('test', ['col1' => 'foo1', 'col2' => 'bar1', 'col3' => 'baz1']); 411 | self::$db->insert('test', ['col1' => 'foo1', 'col2' => 'bar1', 'col3' => 'baz1']); 412 | self::$db->delete_duplicates('test', ['col1'], true, ['id' => 'asc']); 413 | $this->assertEquals(self::$db->fetch_var('SELECT id FROM test LIMIT 1'), 1); 414 | self::$db->clear('test'); 415 | 416 | self::$db->insert('test', ['col1' => 'foo1', 'col2' => 'bar1', 'col3' => null]); 417 | self::$db->insert('test', ['col1' => 'FOO1', 'col2' => 'BAR1', 'col3' => null]); 418 | self::$db->delete_duplicates('test'); 419 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test'), 2); 420 | self::$db->delete_duplicates('test', [], true, [], false); 421 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test'), 1); 422 | self::$db->clear('test'); 423 | 424 | self::$db->insert('test', ['col1' => 'foo1', 'col2' => 'bar1', 'col3' => null]); 425 | self::$db->insert('test', ['col1' => 'foo1', 'col2' => 'bar1', 'col3' => null]); 426 | self::$db->insert('test', ['col1' => 'foo1', 'col2' => 'bar1', 'col3' => null]); 427 | $id = self::$db->insert('test', ['col1' => 'foo1', 'col2' => 'bar1', 'col3' => null]); 428 | self::$db->delete_duplicates('test'); 429 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test'), 1); 430 | $this->assertEquals(self::$db->fetch_var('SELECT id FROM test LIMIT 1'), $id); 431 | self::$db->clear('test'); 432 | } 433 | 434 | function test__trim_values() 435 | { 436 | self::$db->insert('test', ['col1' => 'foo1 ', 'col2' => 'bar1', 'col3' => 'baz1']); 437 | self::$db->insert('test', ['col1' => 'foo2', 'col2' => ' bar2', 'col3' => 'baz2']); 438 | self::$db->insert('test', ['col1' => 'foo2', 'col2' => ' bar2 ', 'col3' => ' baz2 ']); 439 | $this->assertEquals(self::$db->trim_values(), [ 440 | ['table' => 'test', 'column' => 'col1', 'id' => 1, 'before' => 'foo1 ', 'after' => 'foo1'], 441 | ['table' => 'test', 'column' => 'col2', 'id' => 2, 'before' => ' bar2', 'after' => 'bar2'], 442 | ['table' => 'test', 'column' => 'col2', 'id' => 3, 'before' => ' bar2 ', 'after' => 'bar2'], 443 | ['table' => 'test', 'column' => 'col3', 'id' => 3, 'before' => ' baz2 ', 'after' => 'baz2'] 444 | ]); 445 | $this->assertEquals(count(self::$db->trim_values()), 4); 446 | $this->assertEquals(count(self::$db->trim_values(false, ['test'])), 0); 447 | $this->assertEquals(count(self::$db->trim_values(false, ['test' => ['col1', 'col2']])), 1); 448 | $this->assertEquals(count(self::$db->trim_values(true)), 4); 449 | $this->assertEquals(count(self::$db->trim_values(true)), 0); 450 | $this->assertEquals(count(self::$db->trim_values()), 0); 451 | $this->assertEquals(self::$db->fetch_var('SELECT col1 FROM test WHERE id = 1'), 'foo1'); 452 | $this->assertEquals(self::$db->fetch_var('SELECT col2 FROM test WHERE id = 2'), 'bar2'); 453 | $this->assertEquals(self::$db->fetch_var('SELECT col2 FROM test WHERE id = 3'), 'bar2'); 454 | $this->assertEquals(self::$db->fetch_var('SELECT col3 FROM test WHERE id = 3'), 'baz2'); 455 | self::$db->clear('test'); 456 | } 457 | 458 | function test__has_table() 459 | { 460 | $this->assertEquals(self::$db->has_table('test'), true); 461 | $this->assertEquals(self::$db->has_table('test2'), false); 462 | } 463 | 464 | function test__has_column() 465 | { 466 | $this->assertEquals(self::$db->has_column('test', 'col1'), true); 467 | $this->assertEquals(self::$db->has_column('test', 'col0'), false); 468 | } 469 | 470 | function test__get_datatype() 471 | { 472 | $this->assertEquals( 473 | in_array(self::$db->get_datatype('test', 'col1'), ['varchar', 'character varying', 'varchar(255)']), 474 | true 475 | ); 476 | $this->assertEquals(self::$db->get_datatype('test', 'col0'), null); 477 | } 478 | 479 | function test__get_primary_key() 480 | { 481 | $this->assertEquals(self::$db->get_primary_key('test'), 'id'); 482 | $this->assertEquals(self::$db->get_primary_key('test0'), null); 483 | } 484 | 485 | function test__uuid() 486 | { 487 | $uuid1 = self::$db->uuid(); 488 | $uuid2 = self::$db->uuid(); 489 | $this->assertEquals(strlen($uuid1) === 36, true); 490 | $this->assertEquals(strlen($uuid2) === 36, true); 491 | $this->assertEquals($uuid1 === $uuid2, false); 492 | } 493 | 494 | function test__multiple_statements() 495 | { 496 | self::$db->sql->exec(' 497 | INSERT INTO test(col1,col2,col3) VALUES (\'foo\',\'bar\',\'baz\'); 498 | INSERT INTO test(col1,col2,col3) VALUES (\'foo\',\'bar\',\'baz\'); 499 | '); 500 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test'), 2); 501 | } 502 | 503 | function test__errors() 504 | { 505 | try { 506 | self::$db->insert('test', ['id' => 1, 'col1' => (object) ['foo' => 'bar']]); 507 | $this->assertTrue(false); 508 | } catch (\Exception $e) { 509 | $this->assertTrue(true); 510 | } 511 | try { 512 | self::$db->query('SELCET * FROM test'); 513 | $this->assertTrue(false); 514 | } catch (\Exception $e) { 515 | $this->assertTrue(true); 516 | } 517 | $this->assertEquals(self::$db->fetch_var('SELECT COUNT(*) FROM test'), 0); 518 | } 519 | 520 | function test__debug() 521 | { 522 | $this->assertEquals( 523 | self::$db->debug('SELECT * FROM foo WHERE bar = ?', 'baz'), 524 | 'SELECT * FROM foo WHERE bar = \'baz\'' 525 | ); 526 | $this->assertEquals( 527 | self::$db->debug('SELECT * FROM foo WHERE bar = ?', null), 528 | 'SELECT * FROM foo WHERE bar IS NULL' 529 | ); 530 | $this->assertEquals( 531 | self::$db->debug('DELETE FROM tablename WHERE row1 = ?', null), 532 | 'DELETE FROM tablename WHERE row1 IS NULL' 533 | ); 534 | } 535 | } 536 | -------------------------------------------------------------------------------- /src/dbhelper.php: -------------------------------------------------------------------------------- 1 | config = $config; 18 | } 19 | 20 | public function connect( 21 | $driver, 22 | $engine = null, 23 | $host = null, 24 | $username = null, 25 | $password = null, 26 | $database = null, 27 | $port = 3306, 28 | $timeout = 60 29 | ) { 30 | $connect = (object) []; 31 | $sql = null; 32 | switch ($driver) { 33 | case 'pdo': 34 | if ($engine === 'mysql') { 35 | $sql = new PDO( 36 | 'mysql:host=' . 37 | $host . 38 | ';port=' . 39 | $port . 40 | ($database !== null ? ';dbname=' . $database : ';charset=utf8mb4'), 41 | $username, 42 | $password, 43 | [ 44 | PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci', 45 | PDO::ATTR_EMULATE_PREPARES => false, 46 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 47 | PDO::ATTR_TIMEOUT => $timeout 48 | ] 49 | ); 50 | } elseif ($engine === 'postgres') { 51 | $sql = new PDO( 52 | 'pgsql:host=' . $host . ';port=' . $port . ($database !== null ? ';dbname=' . $database : ''), 53 | $username, 54 | $password 55 | ); 56 | //$sql->query('SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci'); 57 | } elseif ($engine === 'sqlite') { 58 | $sql = new PDO('sqlite:' . $host, null, null, [ 59 | PDO::ATTR_EMULATE_PREPARES => false, 60 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 61 | PDO::ATTR_TIMEOUT => $timeout 62 | ]); 63 | } else { 64 | throw new \Exception('missing engine'); 65 | } 66 | $connect->database = $database; 67 | break; 68 | 69 | case 'mysqli': 70 | $sql = new \mysqli($host, $username, $password, $database, $port); 71 | mysqli_set_charset($sql, 'utf8mb4'); 72 | if ($sql->connect_errno) { 73 | die('SQL Connection failed: ' . $sql->connect_error); 74 | } 75 | $connect->database = $database; 76 | break; 77 | 78 | case 'wordpress': 79 | global $wpdb; 80 | $engine = 'mysql'; 81 | $wpdb->show_errors = false; // do not show errors in the frontend, we fire exceptions instead 82 | $wpdb->suppress_errors = false; 83 | $sql = $wpdb; 84 | $connect->database = $wpdb->dbname; 85 | break; 86 | } 87 | $this->sql = $sql; 88 | 89 | $connect->driver = $driver; 90 | $connect->engine = $engine; 91 | $connect->host = $host; 92 | $connect->username = $username; 93 | $connect->password = $password; 94 | $connect->port = $port; 95 | $this->connect = $connect; 96 | } 97 | 98 | public function create_database($database) 99 | { 100 | switch ($this->connect->driver) { 101 | case 'pdo': 102 | if ($this->connect->engine === 'mysql') { 103 | $this->sql->exec('CREATE DATABASE IF NOT EXISTS ' . $database . ';'); 104 | } elseif ($this->connect->engine === 'postgres') { 105 | $this->sql->exec('CREATE DATABASE ' . $database . ';'); 106 | } elseif ($this->connect->engine === 'sqlite') { 107 | @touch($database); 108 | } 109 | break; 110 | 111 | case 'mysqli': 112 | break; 113 | 114 | case 'wordpress': 115 | // TODO 116 | break; 117 | } 118 | } 119 | 120 | public function connect_with_create( 121 | $driver, 122 | $engine = null, 123 | $host = null, 124 | $username = null, 125 | $password = null, 126 | $database = null, 127 | $port = 3306, 128 | $timeout = 60 129 | ) { 130 | $this->connect($driver, $engine, $host, $username, $password, null, $port, $timeout); 131 | $this->create_database($database); 132 | $this->disconnect(); 133 | $this->connect($driver, $engine, $host, $username, $password, $database, $port, $timeout); 134 | } 135 | 136 | public function delete_database($database) 137 | { 138 | switch ($this->connect->driver) { 139 | case 'pdo': 140 | if ($this->connect->engine === 'mysql') { 141 | $this->sql->exec('DROP DATABASE ' . $database . ';'); 142 | } elseif ($this->connect->engine === 'postgres') { 143 | $this->sql->exec('DROP DATABASE ' . $database . ';'); 144 | } elseif ($this->connect->engine === 'sqlite') { 145 | @unlink($database); 146 | } 147 | break; 148 | case 'mysqli': 149 | break; 150 | case 'wordpress': 151 | break; 152 | } 153 | } 154 | 155 | public function disconnect_with_delete() 156 | { 157 | $driver = $this->connect->driver; 158 | $engine = $this->connect->engine; 159 | $host = $this->connect->host; 160 | $username = $this->connect->username; 161 | $password = $this->connect->password; 162 | $database = $this->connect->database; 163 | $port = $this->connect->port; 164 | $this->disconnect(); 165 | if ($driver === 'pdo' && $engine === 'sqlite') { 166 | $this->sql = (object) []; 167 | $this->connect->driver = $driver; 168 | $this->connect->engine = $engine; 169 | $this->connect->host = $host; 170 | $this->delete_database($host); 171 | } else { 172 | $this->connect($driver, $engine, $host, $username, $password, null, $port); 173 | $this->delete_database($database); 174 | $this->disconnect(); 175 | } 176 | } 177 | 178 | public function disconnect() 179 | { 180 | switch ($this->connect->driver) { 181 | case 'pdo': 182 | $this->sql = null; 183 | break; 184 | 185 | case 'mysqli': 186 | $this->sql->close(); 187 | break; 188 | 189 | case 'wordpress': 190 | // TODO 191 | break; 192 | } 193 | } 194 | 195 | public function fetch_all($query) 196 | { 197 | $data = []; 198 | $params = func_get_args(); 199 | unset($params[0]); 200 | $params = array_values($params); 201 | [$query, $params] = $this->preparse_query($query, $params); 202 | 203 | switch ($this->connect->driver) { 204 | case 'pdo': 205 | $stmt = $this->sql->prepare($query); 206 | $stmt->execute($params); 207 | if ($stmt->errorCode() != 0) { 208 | $errors = $stmt->errorInfo(); 209 | throw new \Exception($errors[2]); 210 | } 211 | $data = $stmt->fetchAll(PDO::FETCH_ASSOC); 212 | break; 213 | 214 | case 'mysqli': 215 | // do not use mysqlnd 216 | $result = $this->sql->query($query); 217 | while ($row = $result->fetch_assoc()) { 218 | $data[] = $row; 219 | } 220 | break; 221 | 222 | case 'wordpress': 223 | if (!empty($params)) { 224 | $data = $this->sql->get_results($this->sql->prepare($query, $params)); 225 | } else { 226 | $data = $this->sql->get_results($query); 227 | } 228 | if ($this->sql->last_error) { 229 | throw new \Exception($this->sql->last_error); 230 | } 231 | break; 232 | } 233 | 234 | return $data; 235 | } 236 | 237 | public function fetch_row($query) 238 | { 239 | $data = []; 240 | $params = func_get_args(); 241 | unset($params[0]); 242 | $params = array_values($params); 243 | [$query, $params] = $this->preparse_query($query, $params); 244 | 245 | switch ($this->connect->driver) { 246 | case 'pdo': 247 | $stmt = $this->sql->prepare($query); 248 | $stmt->execute($params); 249 | if ($stmt->errorCode() != 0) { 250 | $errors = $stmt->errorInfo(); 251 | throw new \Exception($errors[2]); 252 | } 253 | $data = $stmt->fetch(PDO::FETCH_ASSOC); 254 | break; 255 | 256 | case 'mysqli': 257 | $data = $this->sql->query($query)->fetch_assoc(); 258 | break; 259 | 260 | case 'wordpress': 261 | if (!empty($params)) { 262 | $data = $this->sql->get_row($this->sql->prepare($query, $params)); 263 | } else { 264 | $data = $this->sql->get_row($query); 265 | } 266 | if ($this->sql->last_error) { 267 | throw new \Exception($this->sql->last_error); 268 | } 269 | break; 270 | } 271 | 272 | return $data; 273 | } 274 | 275 | public function fetch_col($query) 276 | { 277 | $data = []; 278 | $params = func_get_args(); 279 | unset($params[0]); 280 | $params = array_values($params); 281 | [$query, $params] = $this->preparse_query($query, $params); 282 | 283 | switch ($this->connect->driver) { 284 | case 'pdo': 285 | $stmt = $this->sql->prepare($query); 286 | $stmt->execute($params); 287 | if ($stmt->errorCode() != 0) { 288 | $errors = $stmt->errorInfo(); 289 | throw new \Exception($errors[2]); 290 | } 291 | $data = $stmt->fetchAll(PDO::FETCH_ASSOC); 292 | if (!empty($data)) { 293 | $data_tmp = []; 294 | foreach ($data as $dat) { 295 | $data_tmp[] = $dat[array_keys($dat)[0]]; 296 | } 297 | $data = $data_tmp; 298 | } 299 | break; 300 | 301 | case 'mysqli': 302 | // TODO 303 | break; 304 | 305 | case 'wordpress': 306 | if (!empty($params)) { 307 | $data = $this->sql->get_col($this->sql->prepare($query, $params)); 308 | } else { 309 | $data = $this->sql->get_col($query); 310 | } 311 | if ($this->sql->last_error) { 312 | throw new \Exception($this->sql->last_error); 313 | } 314 | break; 315 | } 316 | 317 | return $data; 318 | } 319 | 320 | public function fetch_var($query) 321 | { 322 | $data = []; 323 | $params = func_get_args(); 324 | unset($params[0]); 325 | $params = array_values($params); 326 | [$query, $params] = $this->preparse_query($query, $params); 327 | 328 | switch ($this->connect->driver) { 329 | case 'pdo': 330 | $stmt = $this->sql->prepare($query); 331 | $stmt->execute($params); 332 | if ($stmt->errorCode() != 0) { 333 | $errors = $stmt->errorInfo(); 334 | throw new \Exception($errors[2]); 335 | } 336 | $data = $stmt->fetchObject(); 337 | if (empty($data)) { 338 | return null; 339 | } 340 | $data = (array) $data; 341 | $data = current($data); 342 | break; 343 | 344 | case 'mysqli': 345 | $data = $this->sql->query($query)->fetch_object(); 346 | if (empty($data)) { 347 | return null; 348 | } 349 | $data = (array) $data; 350 | $data = current($data); 351 | break; 352 | 353 | case 'wordpress': 354 | if (!empty($params)) { 355 | $data = $this->sql->get_var($this->sql->prepare($query, $params)); 356 | } else { 357 | $data = $this->sql->get_var($query); 358 | } 359 | if ($this->sql->last_error) { 360 | throw new \Exception($this->sql->last_error); 361 | } 362 | break; 363 | } 364 | 365 | return $data; 366 | } 367 | 368 | public function query($query) 369 | { 370 | $data = []; 371 | $params = func_get_args(); 372 | unset($params[0]); 373 | $params = array_values($params); 374 | [$query, $params] = $this->preparse_query($query, $params); 375 | 376 | if (isset($this->config['auto_inject']) && $this->config['auto_inject'] === true) { 377 | $query = $this->handle_logging($query, $params); 378 | } 379 | 380 | switch ($this->connect->driver) { 381 | case 'pdo': 382 | // in general we use prepare/execute instead of exec or query 383 | // this works certainly in all cases, EXCEPT when doing something like CREATE TABLE, CREATE TRIGGER, DROP TABLE, DROP TRIGGER 384 | // that causes the error "Cannot execute queries while other unbuffered queries are active" 385 | // therefore we switch in those cases to exec 386 | if ( 387 | stripos($query, 'CREATE') === 0 || 388 | stripos($query, 'DROP') === 0 || 389 | stripos($query, 'PRAGMA') === 0 || 390 | stripos($query, 'BEGIN') === 0 391 | ) { 392 | $this->sql->exec($query); 393 | } else { 394 | $stmt = $this->sql->prepare($query); 395 | $stmt->execute($params); 396 | if ($stmt->errorCode() != 0) { 397 | $errors = $stmt->errorInfo(); 398 | throw new \Exception($errors[2]); 399 | } 400 | return $stmt->rowCount(); 401 | } 402 | break; 403 | 404 | case 'mysqli': 405 | $this->sql->query($query); 406 | break; 407 | 408 | case 'wordpress': 409 | if (!empty($params)) { 410 | $this->sql->query($this->sql->prepare($query, $params)); 411 | if ($this->sql->last_error) { 412 | throw new \Exception($this->sql->last_error); 413 | } 414 | return $this->sql->rows_affected; 415 | } else { 416 | $this->sql->query($query); 417 | if ($this->sql->last_error) { 418 | throw new \Exception($this->sql->last_error); 419 | } 420 | return $this->sql->rows_affected; 421 | } 422 | break; 423 | } 424 | } 425 | 426 | public function insert($table, $data) 427 | { 428 | if (!isset($data[0]) && !is_array(array_values($data)[0])) { 429 | $data = [$data]; 430 | } 431 | $query = ''; 432 | $query .= 'INSERT INTO '; 433 | $query .= $this->quote($table); 434 | $query .= '('; 435 | foreach ($data[0] as $data__key => $data__value) { 436 | $query .= $this->quote($data__key); 437 | if (array_keys($data[0])[count(array_keys($data[0])) - 1] !== $data__key) { 438 | $query .= ','; 439 | } 440 | } 441 | $query .= ') '; 442 | $query .= 'VALUES '; 443 | foreach ($data as $data__key => $data__value) { 444 | $query .= '('; 445 | $query .= str_repeat('?,', count($data__value) - 1) . '?'; 446 | $query .= ')'; 447 | if (array_keys($data)[count(array_keys($data)) - 1] !== $data__key) { 448 | $query .= ','; 449 | } 450 | } 451 | $args = []; 452 | $args[] = $query; 453 | foreach ($data as $data__key => $data__value) { 454 | foreach ($data__value as $data__value__key => $data__value__value) { 455 | // because pdo can't insert true/false values so easily, convert them to 1/0 456 | if ($data__value__value === true) { 457 | $data__value__value = 1; 458 | } 459 | if ($data__value__value === false) { 460 | $data__value__value = 0; 461 | } 462 | $args[] = $data__value__value; 463 | } 464 | } 465 | $ret = call_user_func_array([$this, 'query'], $args); 466 | 467 | // if we created an item without an auto-incremented primary key, 468 | // sql does not return the last inserted id properly. 469 | // we instead return the data already delivered! 470 | $primary_key = $this->get_primary_key($table); 471 | if ($primary_key && isset($data[0]) && isset($data[0][$primary_key])) { 472 | return $data[0][$primary_key]; 473 | } 474 | 475 | // mysql returns the last inserted id inside the current session, obviously ignoring triggers 476 | // on postgres we cannot use LASTVAL(), because it returns the last id of possibly inserted rows caused by triggers 477 | // see: https://stackoverflow.com/questions/51558021/mysql-postgres-last-insert-id-lastval-different-behaviour 478 | if ($this->connect->engine === 'mysql') { 479 | return $this->last_insert_id(); 480 | } 481 | if ($this->connect->engine === 'postgres') { 482 | return $this->last_insert_id($table, $this->get_primary_key($table)); 483 | } 484 | if ($this->connect->engine === 'sqlite') { 485 | return $this->last_insert_id(); 486 | } 487 | } 488 | 489 | public function last_insert_id($table = null, $column = null) 490 | { 491 | $last_insert_id = null; 492 | switch ($this->connect->driver) { 493 | case 'pdo': 494 | if ($this->connect->engine == 'mysql') { 495 | try { 496 | $last_insert_id = $this->fetch_var('SELECT LAST_INSERT_ID();'); 497 | } catch (\Exception $e) { 498 | $last_insert_id = null; 499 | } 500 | } 501 | if ($this->connect->engine == 'postgres') { 502 | try { 503 | if ($table === null || $column === null) { 504 | $last_insert_id = $this->fetch_var('SELECT LASTVAL();'); 505 | } else { 506 | $last_insert_id = $this->fetch_var( 507 | "SELECT CURRVAL(pg_get_serial_sequence('" . $table . "','" . $column . "'));" 508 | ); 509 | } 510 | } catch (\Exception $e) { 511 | $last_insert_id = null; 512 | } 513 | } 514 | if ($this->connect->engine == 'sqlite') { 515 | try { 516 | $last_insert_id = $this->fetch_var('SELECT last_insert_rowid();'); 517 | } catch (\Exception $e) { 518 | $last_insert_id = null; 519 | } 520 | } 521 | break; 522 | 523 | case 'mysqli': 524 | $last_insert_id = mysqli_insert_id($this->sql); 525 | break; 526 | 527 | case 'wordpress': 528 | $last_insert_id = $this->fetch_var('SELECT LAST_INSERT_ID();'); 529 | break; 530 | } 531 | 532 | return $last_insert_id; 533 | } 534 | 535 | public function update($table, $data, $condition = null) 536 | { 537 | if (isset($data[0]) && is_array($data[0])) { 538 | return $this->update_batch($table, $data); 539 | } 540 | $query = ''; 541 | $query .= 'UPDATE '; 542 | $query .= $this->quote($table); 543 | $query .= ' SET '; 544 | foreach ($data as $key => $value) { 545 | $query .= $this->quote($key); 546 | $query .= ' = '; 547 | $query .= '?'; 548 | end($data); 549 | if ($key !== key($data)) { 550 | $query .= ', '; 551 | } 552 | } 553 | $query .= ' WHERE '; 554 | foreach ($condition as $key => $value) { 555 | $query .= $this->quote($key); 556 | $query .= ' = '; 557 | $query .= '? '; 558 | end($condition); 559 | if ($key !== key($condition)) { 560 | $query .= ' AND '; 561 | } 562 | } 563 | $args = []; 564 | $args[] = $query; 565 | foreach ($data as $d) { 566 | if ($d === true) { 567 | $d = 1; 568 | } 569 | if ($d === false) { 570 | $d = 0; 571 | } 572 | $args[] = $d; 573 | } 574 | foreach ($condition as $c) { 575 | if ($c === true) { 576 | $c = 1; 577 | } 578 | if ($c === false) { 579 | $c = 0; 580 | } 581 | $args[] = $c; 582 | } 583 | return call_user_func_array([$this, 'query'], $args); // returns the affected row counts 584 | } 585 | 586 | public function delete($table, $conditions) 587 | { 588 | if (!isset($conditions[0]) && !is_array(array_values($conditions)[0])) { 589 | $conditions = [$conditions]; 590 | } 591 | $query = ''; 592 | $query .= 'DELETE FROM '; 593 | $query .= $this->quote($table); 594 | $query .= ' WHERE '; 595 | $query .= '('; 596 | foreach ($conditions as $conditions__key => $conditions__value) { 597 | $query .= '('; 598 | foreach ($conditions__value as $conditions__value__key => $conditions__value__value) { 599 | $query .= $this->quote($conditions__value__key); 600 | $query .= ' = '; 601 | $query .= '?'; 602 | if ( 603 | array_keys($conditions__value)[count(array_keys($conditions__value)) - 1] !== 604 | $conditions__value__key 605 | ) { 606 | $query .= ' AND '; 607 | } 608 | } 609 | $query .= ')'; 610 | if (array_keys($conditions)[count(array_keys($conditions)) - 1] !== $conditions__key) { 611 | $query .= ' OR '; 612 | } 613 | } 614 | $query .= ')'; 615 | $args = []; 616 | $args[] = $query; 617 | foreach ($conditions as $conditions__key => $conditions__value) { 618 | foreach ($conditions__value as $conditions__value__key => $conditions__value__value) { 619 | if ($conditions__value__value === true) { 620 | $conditions__value__value = 1; 621 | } 622 | if ($conditions__value__value === false) { 623 | $conditions__value__value = 0; 624 | } 625 | $args[] = $conditions__value__value; 626 | } 627 | } 628 | return call_user_func_array([$this, 'query'], $args); // returns the affected row counts 629 | } 630 | 631 | public function clear($table = null) 632 | { 633 | if ($table === null) { 634 | if ($this->connect->engine === 'mysql') { 635 | $this->query('SET FOREIGN_KEY_CHECKS = 0'); 636 | $tables = $this->fetch_col( 637 | 'SELECT table_name FROM information_schema.tables WHERE table_schema = ?', 638 | $this->connect->database 639 | ); 640 | if (!empty($tables)) { 641 | foreach ($tables as $tables__value) { 642 | $this->query('DROP TABLE ' . $tables__value); 643 | } 644 | } 645 | $this->query('SET FOREIGN_KEY_CHECKS = 1'); 646 | } elseif ($this->connect->engine === 'postgres') { 647 | $this->query('DROP SCHEMA public CASCADE'); 648 | $this->query('CREATE SCHEMA public'); 649 | } elseif ($this->connect->engine === 'sqlite') { 650 | $db_driver = $this->connect->driver; 651 | $db_engine = $this->connect->engine; 652 | $db_file = $this->connect->host; 653 | $this->disconnect(); 654 | unlink($db_file); 655 | $this->connect($db_driver, $db_engine, $db_file); 656 | } 657 | } else { 658 | if ($this->connect->engine === 'mysql') { 659 | $this->query('TRUNCATE TABLE ' . $table); 660 | } elseif ($this->connect->engine === 'postgres') { 661 | $this->query('TRUNCATE TABLE ' . $table . ' RESTART IDENTITY'); 662 | } elseif ($this->connect->engine === 'sqlite') { 663 | $this->query('DELETE FROM ' . $table); 664 | $this->query('VACUUM'); 665 | } 666 | } 667 | } 668 | 669 | public function delete_table($table) 670 | { 671 | $this->query('DROP TABLE ' . $table); 672 | } 673 | 674 | public function create_table($table, $cols) 675 | { 676 | $query = ''; 677 | $query .= 'CREATE TABLE IF NOT EXISTS '; 678 | $query .= $table . ' '; 679 | $query .= '('; 680 | foreach ($cols as $cols__key => $cols__value) { 681 | // quote "bar" in FOO (bar) 682 | if (preg_match('/(.*)\((.*)\)(.*)/', $cols__key, $matches)) { 683 | $cols__key = $matches[1] . '(' . $this->quote($matches[2]) . ')' . $matches[3]; 684 | } else { 685 | $cols__key = $this->quote($cols__key); 686 | } 687 | $query .= $cols__key; 688 | $query .= ' '; 689 | $query .= $cols__value; 690 | $query .= ','; 691 | } 692 | $query = substr($query, 0, -1); 693 | $query .= ')'; 694 | $this->query($query); 695 | } 696 | 697 | public function get_tables() 698 | { 699 | if ($this->connect->engine === 'mysql') { 700 | return $this->fetch_col( 701 | 'SELECT table_name FROM information_schema.tables WHERE table_catalog = ? AND table_schema = ? ORDER BY table_name', 702 | 'def', 703 | $this->connect->database 704 | ); 705 | } elseif ($this->connect->engine === 'postgres') { 706 | return $this->fetch_col( 707 | 'SELECT table_name FROM information_schema.tables WHERE table_catalog = ? AND table_schema = ? ORDER BY table_name', 708 | $this->connect->database, 709 | 'public' 710 | ); 711 | } elseif ($this->connect->engine === 'sqlite') { 712 | return $this->fetch_col('SELECT name FROM sqlite_master WHERE type = ?', 'table'); 713 | } 714 | } 715 | 716 | public function get_columns($table) 717 | { 718 | if ($this->connect->engine === 'mysql') { 719 | return $this->fetch_col( 720 | 'SELECT column_name FROM information_schema.columns WHERE table_catalog = ? AND table_schema = ? AND table_name = ? ORDER BY ORDINAL_POSITION', 721 | 'def', 722 | $this->connect->database, 723 | $table 724 | ); 725 | } elseif ($this->connect->engine === 'postgres') { 726 | return $this->fetch_col( 727 | 'SELECT column_name FROM information_schema.columns WHERE table_catalog = ? AND table_schema = ? AND table_name = ? ORDER BY ORDINAL_POSITION', 728 | $this->connect->database, 729 | 'public', 730 | $table 731 | ); 732 | } elseif ($this->connect->engine === 'sqlite') { 733 | $pragma = $this->fetch_all('PRAGMA table_info(' . $this->quote($table) . ');'); 734 | $cols = []; 735 | foreach ($pragma as $pragma__value) { 736 | $cols[] = $pragma__value['name']; 737 | } 738 | return $cols; 739 | } 740 | } 741 | 742 | public function get_foreign_keys($table) 743 | { 744 | if ($this->connect->engine === 'mysql') { 745 | $return = []; 746 | $cols = $this->fetch_all( 747 | ' 748 | SELECT 749 | kcu.column_name AS column_name, 750 | kcu.referenced_table_name as foreign_table_name, 751 | kcu.referenced_column_name as foreign_column_name 752 | FROM 753 | information_schema.table_constraints AS tc 754 | JOIN information_schema.key_column_usage AS kcu 755 | ON tc.constraint_name = kcu.constraint_name 756 | AND tc.table_schema = kcu.table_schema 757 | WHERE 758 | tc.constraint_type = ? AND 759 | tc.table_schema = ? AND 760 | tc.table_name = ? 761 | ', 762 | 'FOREIGN KEY', 763 | $this->connect->database, 764 | $table 765 | ); 766 | foreach ($cols as $cols__value) { 767 | $return[$cols__value['column_name']] = [ 768 | $cols__value['foreign_table_name'], 769 | $cols__value['foreign_column_name'] 770 | ]; 771 | } 772 | return $return; 773 | } elseif ($this->connect->engine === 'postgres') { 774 | $return = []; 775 | $cols = $this->fetch_all( 776 | ' 777 | SELECT 778 | kcu.column_name AS column_name, 779 | ccu.table_name AS foreign_table_name, 780 | ccu.column_name AS foreign_column_name 781 | FROM 782 | information_schema.table_constraints AS tc 783 | JOIN information_schema.key_column_usage AS kcu 784 | ON tc.constraint_name = kcu.constraint_name 785 | AND tc.table_schema = kcu.table_schema 786 | JOIN information_schema.constraint_column_usage AS ccu 787 | ON ccu.constraint_name = tc.constraint_name 788 | AND ccu.table_schema = tc.table_schema 789 | WHERE 790 | tc.constraint_type = ? AND 791 | tc.table_catalog = ? AND 792 | tc.table_schema = ? AND 793 | tc.table_name = ? 794 | ', 795 | 'FOREIGN KEY', 796 | $this->connect->database, 797 | 'public', 798 | $table 799 | ); 800 | foreach ($cols as $cols__value) { 801 | $return[$cols__value['column_name']] = [ 802 | $cols__value['foreign_table_name'], 803 | $cols__value['foreign_column_name'] 804 | ]; 805 | } 806 | return $return; 807 | } elseif ($this->connect->engine === 'sqlite') { 808 | $pragma = $this->fetch_all('PRAGMA foreign_key_list(' . $this->quote($table) . ');'); 809 | $return = []; 810 | foreach ($pragma as $pragma__value) { 811 | $return[$pragma__value['from']] = [$pragma__value['table'], $pragma__value['to']]; 812 | } 813 | return $return; 814 | } 815 | } 816 | 817 | public function is_foreign_key($table, $column) 818 | { 819 | return array_key_exists($column, $this->get_foreign_keys($table)); 820 | } 821 | 822 | public function get_foreign_tables_out($table) 823 | { 824 | $return = []; 825 | foreach ($this->get_foreign_keys($table) as $foreign_keys__key => $foreign_keys__value) { 826 | if (!array_key_exists($foreign_keys__value[0], $return)) { 827 | $return[$foreign_keys__value[0]] = []; 828 | } 829 | $return[$foreign_keys__value[0]][] = [$foreign_keys__key, $foreign_keys__value[1]]; 830 | } 831 | return $return; 832 | } 833 | 834 | public function get_foreign_tables_in($table) 835 | { 836 | $return = []; 837 | $tables = $this->get_tables(); 838 | foreach ($tables as $tables__value) { 839 | if ($tables__value === $table) { 840 | continue; 841 | } 842 | foreach ($this->get_foreign_tables_out($tables__value) as $foreign_tables__key => $foreign_tables__value) { 843 | if ($foreign_tables__key !== $table) { 844 | continue; 845 | } 846 | if (!array_key_exists($tables__value, $return)) { 847 | $return[$tables__value] = []; 848 | } 849 | $return[$tables__value] = array_merge($return[$tables__value], $foreign_tables__value); 850 | } 851 | } 852 | return $return; 853 | } 854 | 855 | public function has_table($table) 856 | { 857 | return in_array($table, $this->get_tables()); 858 | } 859 | 860 | public function has_column($table, $column) 861 | { 862 | return in_array($column, $this->get_columns($table)); 863 | } 864 | 865 | public function get_datatype($table, $column) 866 | { 867 | if ($this->connect->engine === 'mysql') { 868 | return $this->fetch_var( 869 | 'SELECT data_type FROM information_schema.columns WHERE table_catalog = ? AND table_schema = ? AND table_name = ? and column_name = ?', 870 | 'def', 871 | $this->connect->database, 872 | $table, 873 | $column 874 | ); 875 | } elseif ($this->connect->engine === 'postgres') { 876 | return $this->fetch_var( 877 | 'SELECT data_type FROM information_schema.columns WHERE table_catalog = ? AND table_schema = ? AND table_name = ? and column_name = ?', 878 | $this->connect->database, 879 | 'public', 880 | $table, 881 | $column 882 | ); 883 | } elseif ($this->connect->engine === 'sqlite') { 884 | $pragma = $this->fetch_all('PRAGMA table_info(' . $this->quote($table) . ');'); 885 | foreach ($pragma as $pragma__value) { 886 | if ($pragma__value['name'] === $column) { 887 | return $pragma__value['type']; 888 | } 889 | } 890 | return null; 891 | } 892 | } 893 | 894 | public function get_primary_key($table) 895 | { 896 | try { 897 | if ($this->connect->engine === 'mysql') { 898 | return ((object) $this->fetch_row( 899 | 'SHOW KEYS FROM ' . $this->quote($table) . ' WHERE Key_name = ?', 900 | 'PRIMARY' 901 | ))->Column_name; 902 | } 903 | if ($this->connect->engine === 'postgres') { 904 | return $this->fetch_var( 905 | 'SELECT pg_attribute.attname FROM pg_index JOIN pg_attribute ON pg_attribute.attrelid = pg_index.indrelid AND pg_attribute.attnum = ANY(pg_index.indkey) WHERE pg_index.indrelid = \'' . 906 | $table . 907 | '\'::regclass AND pg_index.indisprimary' 908 | ); 909 | } 910 | if ($this->connect->engine === 'sqlite') { 911 | $pragma = $this->fetch_all('PRAGMA table_info(' . $this->quote($table) . ');'); 912 | foreach ($pragma as $pragma__value) { 913 | if ($pragma__value['pk'] == 1) { 914 | return $pragma__value['name']; 915 | } 916 | } 917 | return null; 918 | } 919 | } catch (\Exception $e) { 920 | return null; 921 | } 922 | } 923 | 924 | public function count($table, $condition = []) 925 | { 926 | $query = ''; 927 | $query .= 'SELECT COUNT(*) FROM '; 928 | $query .= $this->quote($table); 929 | if (!empty($condition)) { 930 | $query .= ' WHERE '; 931 | foreach ($condition as $key => $value) { 932 | $query .= $this->quote($key); 933 | $query .= ' = '; 934 | $query .= '? '; 935 | end($condition); 936 | if ($key !== key($condition)) { 937 | $query .= ' AND '; 938 | } 939 | } 940 | } 941 | $args = []; 942 | if (!empty($condition)) { 943 | foreach ($condition as $c) { 944 | if ($c === true) { 945 | $c = 1; 946 | } 947 | if ($c === false) { 948 | $c = 0; 949 | } 950 | $args[] = $c; 951 | } 952 | } 953 | $ret = $this->fetch_var($query, $args); 954 | if (is_numeric($ret)) { 955 | $ret = intval($ret); 956 | } 957 | return $ret; 958 | } 959 | 960 | public function trim_values($update = false, $ignore = []) 961 | { 962 | if (!empty($ignore)) { 963 | $ignore_prev = $ignore; 964 | foreach ($ignore_prev as $ignore_prev__key => $ignore_prev__value) { 965 | if (is_string($ignore_prev__value)) { 966 | $ignore[$ignore_prev__value] = null; 967 | } elseif (is_array($ignore_prev__value)) { 968 | $ignore[$ignore_prev__key] = $ignore_prev__value; 969 | } 970 | } 971 | } 972 | $return = []; 973 | foreach ($this->get_tables() as $tables__value) { 974 | $query = ''; 975 | $query .= 'SELECT * FROM ' . $tables__value . ' WHERE '; 976 | $query_or = []; 977 | foreach ($this->get_columns($tables__value) as $columns__value) { 978 | if ($this->connect->engine === 'sqlite') { 979 | $query_or[] = 980 | 'CAST(' . 981 | $this->quote($columns__value) . 982 | ' AS TEXT) LIKE \' %\' OR CAST(' . 983 | $this->quote($columns__value) . 984 | ' AS TEXT) LIKE \'% \''; 985 | } else { 986 | $query_or[] = 987 | 'CONCAT(' . 988 | $this->quote($columns__value) . 989 | ',\'\') LIKE \' %\' OR CONCAT(' . 990 | $this->quote($columns__value) . 991 | ',\'\') LIKE \'% \''; 992 | } 993 | } 994 | $query .= implode(' OR ', $query_or); 995 | $result = $this->fetch_all($query); 996 | if (!empty($result)) { 997 | foreach ($result as $result__value) { 998 | $id = $result__value[$this->get_primary_key($tables__value)]; 999 | foreach ($result__value as $result__value__key => $result__value__value) { 1000 | if ( 1001 | !preg_match('/^ .+$/', $result__value__value) && 1002 | !preg_match('/^.+ $/', $result__value__value) 1003 | ) { 1004 | continue; 1005 | } 1006 | if ( 1007 | !empty($ignore) && 1008 | array_key_exists($tables__value, $ignore) && 1009 | ($ignore[$tables__value] === null || 1010 | (is_array($ignore[$tables__value]) && 1011 | in_array($result__value__key, $ignore[$tables__value]))) 1012 | ) { 1013 | continue; 1014 | } 1015 | $return[] = [ 1016 | 'table' => $tables__value, 1017 | 'column' => $result__value__key, 1018 | 'id' => $id, 1019 | 'before' => $result__value__value, 1020 | 'after' => trim($result__value__value) 1021 | ]; 1022 | if ($update === true) { 1023 | $this->update( 1024 | $tables__value, 1025 | [$result__value__key => trim($result__value__value)], 1026 | [ 1027 | $this->get_primary_key($tables__value) => $id, 1028 | $result__value__key => $result__value__value 1029 | ] 1030 | ); 1031 | } 1032 | } 1033 | } 1034 | } 1035 | } 1036 | usort($return, function ($a, $b) { 1037 | return strcmp( 1038 | $a['table'] . '.' . $a['column'] . '.' . $a['before'], 1039 | $b['table'] . '.' . $b['column'] . '.' . $a['before'] 1040 | ); 1041 | }); 1042 | return $return; 1043 | } 1044 | 1045 | public function get_duplicates() 1046 | { 1047 | $duplicates_data = []; 1048 | $duplicates_count = []; 1049 | $tables = $this->get_tables(); 1050 | foreach ($tables as $tables__value) { 1051 | $primaryKey = $this->get_primary_key($tables__value); 1052 | if ($primaryKey == '') { 1053 | continue; 1054 | } 1055 | $columns = []; 1056 | foreach ($this->get_columns($tables__value) as $columns__value) { 1057 | if ($columns__value === $primaryKey) { 1058 | continue; 1059 | } 1060 | $columns[] = $this->quote($columns__value); 1061 | } 1062 | $duplicates_this = $this->fetch_all( 1063 | 'SELECT ' . 1064 | implode(', ', $columns) . 1065 | ', MIN(' . 1066 | $this->quote($primaryKey) . 1067 | ') as ' . 1068 | $this->quote('MIN()') . 1069 | ', COUNT(*) as ' . 1070 | $this->quote('COUNT()') . 1071 | ' FROM ' . 1072 | $tables__value . 1073 | ' GROUP BY ' . 1074 | implode(', ', $columns) . 1075 | ' HAVING COUNT(*) > 1' 1076 | ); 1077 | if (!empty($duplicates_this)) { 1078 | $duplicates_data[$tables__value] = $duplicates_this; 1079 | $duplicates_count[$tables__value] = 0; 1080 | foreach ($duplicates_this as $duplicates_this__value) { 1081 | $duplicates_count[$tables__value] += $duplicates_this__value['COUNT()']; 1082 | } 1083 | } 1084 | } 1085 | return ['count' => $duplicates_count, 'data' => $duplicates_data]; 1086 | } 1087 | 1088 | public function delete_duplicates( 1089 | $table, 1090 | $cols = [], 1091 | $match_null_values = true, 1092 | $primary = [], 1093 | $case_sensitivity = true 1094 | ) { 1095 | if (empty($primary)) { 1096 | $primary = [$this->get_primary_key($table) => 'desc']; 1097 | } 1098 | $primary_key = array_keys($primary)[0]; 1099 | $primary_order = $primary[$primary_key]; 1100 | 1101 | if (empty($cols)) { 1102 | $cols = $this->get_columns($table); 1103 | $cols = array_filter($cols, function ($cols__value) use ($primary_key) { 1104 | return $cols__value !== $primary_key; 1105 | }); 1106 | } 1107 | 1108 | $ret = 1; 1109 | while ($ret > 0) { 1110 | $query = ''; 1111 | $query .= 'DELETE FROM ' . $this->quote($table) . ' '; 1112 | $query .= 'WHERE ' . $this->quote($primary_key) . ' IN ('; 1113 | $query .= 'SELECT * FROM ('; 1114 | $query .= 1115 | 'SELECT ' . 1116 | ($primary_order === 'desc' ? 'MIN' : 'MAX') . 1117 | '(' . 1118 | $this->quote($primary_key) . 1119 | ') FROM ' . 1120 | $this->quote($table) . 1121 | ' GROUP BY '; 1122 | $query .= implode( 1123 | ', ', 1124 | array_map(function ($cols__value) use ($match_null_values, $primary_key, $case_sensitivity) { 1125 | $ret = ''; 1126 | if ($match_null_values === false) { 1127 | $ret .= 'COALESCE(CAST('; 1128 | } 1129 | // postgres and sqlite do a case sensitive group by by default 1130 | // on mysql we need the following modification 1131 | if ($this->connect->engine === 'mysql') { 1132 | // variant 1: Cast as binary (we use md5, because its neater) 1133 | //$ret .= 'CAST('; 1134 | // variant 2: MD5 trick 1135 | $ret .= 'MD5('; 1136 | } 1137 | if ($case_sensitivity === false) { 1138 | $ret .= 'LOWER('; 1139 | } 1140 | $ret .= $this->quote($cols__value); 1141 | if ($case_sensitivity === false) { 1142 | $ret .= ')'; 1143 | } 1144 | if ($this->connect->engine === 'mysql') { 1145 | //$ret .= ' AS BINARY)'; 1146 | $ret .= ')'; 1147 | } 1148 | if ($match_null_values === false) { 1149 | $ret .= ' AS CHAR), CAST(' . $this->quote($primary_key) . ' AS CHAR))'; 1150 | } 1151 | return $ret; 1152 | }, $cols) 1153 | ); 1154 | $query .= 'HAVING COUNT(*) > 1'; 1155 | $query .= ') as tmp'; 1156 | $query .= ')'; 1157 | $ret = $this->query($query); 1158 | } 1159 | 1160 | // the approach has massive performance issues not work on some mariadb dbs 1161 | /* 1162 | $query = ''; 1163 | $query .= 'DELETE FROM ' . $this->quote($table) . ' '; 1164 | $query .= 'WHERE ' . $this->quote($primary_key) . ' NOT IN ('; 1165 | $query .= 'SELECT * FROM ('; 1166 | $query .= 1167 | 'SELECT ' . 1168 | ($primary_order === 'desc' ? 'MAX' : 'MIN') . 1169 | '(' . 1170 | $this->quote($primary_key) . 1171 | ') FROM ' . 1172 | $this->quote($table) . 1173 | ' GROUP BY '; 1174 | $query .= implode( 1175 | ', ', 1176 | array_map(function ($cols__value) use ($match_null_values, $primary_key, $case_sensitivity) { 1177 | $ret = ''; 1178 | if ($match_null_values === false) { 1179 | $ret .= 'COALESCE(CAST('; 1180 | } 1181 | // postgres and sqlite do a case sensitive group by by default 1182 | // on mysql we need the following modification 1183 | if ($this->connect->engine === 'mysql') { 1184 | // variant 1: Cast as binary (we use md5, because its neater) 1185 | //$ret .= 'CAST('; 1186 | // variant 2: MD5 trick 1187 | $ret .= 'MD5('; 1188 | } 1189 | if ($case_sensitivity === false) { 1190 | $ret .= 'LOWER('; 1191 | } 1192 | $ret .= $this->quote($cols__value); 1193 | if ($case_sensitivity === false) { 1194 | $ret .= ')'; 1195 | } 1196 | if ($this->connect->engine === 'mysql') { 1197 | //$ret .= ' AS BINARY)'; 1198 | $ret .= ')'; 1199 | } 1200 | if ($match_null_values === false) { 1201 | $ret .= ' AS CHAR), CAST(' . $this->quote($primary_key) . ' AS CHAR))'; 1202 | } 1203 | return $ret; 1204 | }, $cols) 1205 | ); 1206 | $query .= ') as tmp'; 1207 | $query .= ')'; 1208 | */ 1209 | } 1210 | 1211 | public function enable_auto_inject() 1212 | { 1213 | $this->config['auto_inject'] = true; 1214 | } 1215 | 1216 | public function setup_logging() 1217 | { 1218 | if ($this->connect->engine === 'mysql') { 1219 | $this->setup_logging_create_table_mysql(); 1220 | $this->setup_logging_add_column(); 1221 | $this->setup_logging_create_triggers_mysql(); 1222 | } 1223 | if ($this->connect->engine === 'postgres') { 1224 | $this->setup_logging_create_table_postgres(); 1225 | $this->setup_logging_add_column(); 1226 | $this->setup_logging_create_triggers_postgres(); 1227 | } 1228 | 1229 | $this->setup_logging_delete_older(); 1230 | } 1231 | 1232 | private function setup_logging_delete_older() 1233 | { 1234 | if (isset($this->config['delete_older']) && is_numeric($this->config['delete_older'])) { 1235 | $this->query( 1236 | 'DELETE FROM ' . $this->config['logging_table'] . ' WHERE updated_at < ?', 1237 | date('Y-m-d', strtotime('now - ' . $this->config['delete_older'] . ' months')) 1238 | ); 1239 | } 1240 | } 1241 | 1242 | private function setup_logging_create_table_mysql() 1243 | { 1244 | $this->query( 1245 | ' 1246 | CREATE TABLE IF NOT EXISTS ' . 1247 | $this->config['logging_table'] . 1248 | ' ( 1249 | id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 1250 | log_event varchar(10) NOT NULL, 1251 | log_table varchar(100) NOT NULL, 1252 | log_key varchar(100) NOT NULL, 1253 | log_column varchar(100) DEFAULT NULL, 1254 | log_value LONGTEXT DEFAULT NULL, 1255 | log_uuid varchar(36) DEFAULT NULL, 1256 | updated_by varchar(100) DEFAULT NULL, 1257 | updated_at datetime(0) DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) NOT NULL 1258 | ) 1259 | ' 1260 | ); 1261 | } 1262 | 1263 | private function setup_logging_create_table_postgres() 1264 | { 1265 | $this->query( 1266 | ' 1267 | CREATE TABLE IF NOT EXISTS ' . 1268 | $this->config['logging_table'] . 1269 | ' ( 1270 | id SERIAL NOT NULL PRIMARY KEY, 1271 | log_event varchar(10) NOT NULL, 1272 | log_table varchar(100) NOT NULL, 1273 | log_key varchar(100) NOT NULL, 1274 | log_column varchar(100) DEFAULT NULL, 1275 | log_value TEXT DEFAULT NULL, 1276 | log_uuid varchar(36) DEFAULT NULL, 1277 | updated_by varchar(100) DEFAULT NULL, 1278 | updated_at TIMESTAMP without time zone NULL 1279 | ) 1280 | ' 1281 | ); 1282 | $this->query(' 1283 | CREATE OR REPLACE FUNCTION auto_update_updated_at_column() 1284 | RETURNS TRIGGER AS $$ 1285 | BEGIN 1286 | NEW.updated_at = now(); 1287 | RETURN NEW; 1288 | END; 1289 | $$ language \'plpgsql\'; 1290 | '); 1291 | $this->query( 1292 | 'DROP TRIGGER IF EXISTS ' . 1293 | $this->quote('auto_update_updated_at_column_on_insert') . 1294 | ' ON ' . 1295 | $this->config['logging_table'] 1296 | ); 1297 | $this->query( 1298 | 'DROP TRIGGER IF EXISTS ' . 1299 | $this->quote('auto_update_updated_at_column_on_update') . 1300 | ' ON ' . 1301 | $this->config['logging_table'] 1302 | ); 1303 | $this->query( 1304 | 'CREATE TRIGGER auto_update_updated_at_column_on_insert BEFORE INSERT ON ' . 1305 | $this->config['logging_table'] . 1306 | ' FOR EACH ROW EXECUTE PROCEDURE auto_update_updated_at_column();' 1307 | ); 1308 | $this->query( 1309 | 'CREATE TRIGGER auto_update_updated_at_column_on_update BEFORE UPDATE ON ' . 1310 | $this->config['logging_table'] . 1311 | ' FOR EACH ROW EXECUTE PROCEDURE auto_update_updated_at_column();' 1312 | ); 1313 | } 1314 | 1315 | private function setup_logging_add_column() 1316 | { 1317 | foreach ($this->get_tables() as $table__value) { 1318 | if ( 1319 | isset($this->config['exclude']) && 1320 | isset($this->config['exclude']['tables']) && 1321 | in_array($table__value, $this->config['exclude']['tables']) 1322 | ) { 1323 | continue; 1324 | } 1325 | if ($table__value === $this->config['logging_table']) { 1326 | continue; 1327 | } 1328 | if (!$this->has_column($table__value, 'updated_by')) { 1329 | $this->query('ALTER TABLE ' . $table__value . ' ADD COLUMN updated_by varchar(50)'); 1330 | } 1331 | } 1332 | } 1333 | 1334 | public function disable_logging() 1335 | { 1336 | foreach ($this->get_tables() as $table__value) { 1337 | if ($this->connect->engine === 'mysql') { 1338 | $this->query('DROP TRIGGER IF EXISTS ' . $this->quote('trigger-logging-insert-' . $table__value)); 1339 | $this->query('DROP TRIGGER IF EXISTS ' . $this->quote('trigger-logging-update-' . $table__value)); 1340 | $this->query('DROP TRIGGER IF EXISTS ' . $this->quote('trigger-logging-delete-' . $table__value)); 1341 | } 1342 | if ($this->connect->engine === 'postgres') { 1343 | $this->query( 1344 | 'DROP TRIGGER IF EXISTS ' . 1345 | $this->quote('trigger-logging-insert-' . $table__value) . 1346 | ' ON ' . 1347 | $this->quote($table__value) 1348 | ); 1349 | $this->query( 1350 | 'DROP TRIGGER IF EXISTS ' . 1351 | $this->quote('trigger-logging-update-' . $table__value) . 1352 | ' ON ' . 1353 | $this->quote($table__value) 1354 | ); 1355 | $this->query( 1356 | 'DROP TRIGGER IF EXISTS ' . 1357 | $this->quote('trigger-logging-delete-' . $table__value) . 1358 | ' ON ' . 1359 | $this->quote($table__value) 1360 | ); 1361 | } 1362 | } 1363 | } 1364 | 1365 | public function enable_logging() 1366 | { 1367 | if ($this->connect->engine === 'mysql') { 1368 | $this->setup_logging_create_triggers_mysql(); 1369 | } 1370 | if ($this->connect->engine === 'postgres') { 1371 | $this->setup_logging_create_triggers_postgres(); 1372 | } 1373 | } 1374 | 1375 | private function setup_logging_create_triggers_mysql() 1376 | { 1377 | foreach ($this->get_tables() as $table__value) { 1378 | $this->query('DROP TRIGGER IF EXISTS ' . $this->quote('trigger-logging-insert-' . $table__value)); 1379 | $this->query('DROP TRIGGER IF EXISTS ' . $this->quote('trigger-logging-update-' . $table__value)); 1380 | $this->query('DROP TRIGGER IF EXISTS ' . $this->quote('trigger-logging-delete-' . $table__value)); 1381 | 1382 | if ( 1383 | isset($this->config['exclude']) && 1384 | isset($this->config['exclude']['tables']) && 1385 | in_array($table__value, $this->config['exclude']['tables']) 1386 | ) { 1387 | continue; 1388 | } 1389 | if ($table__value === $this->config['logging_table']) { 1390 | continue; 1391 | } 1392 | 1393 | $primary_key = $this->get_primary_key($table__value); 1394 | 1395 | /* note: we do not use DELIMITER $$ here, because php in mysql can handle that anyways because it does not execute multiple queries */ 1396 | 1397 | $query = 1398 | ' 1399 | CREATE TRIGGER ' . 1400 | $this->quote('trigger-logging-insert-' . $table__value) . 1401 | ' 1402 | AFTER INSERT ON ' . 1403 | $table__value . 1404 | ' FOR EACH ROW 1405 | BEGIN 1406 | DECLARE uuid TEXT; 1407 | SET @uuid := ' . 1408 | $this->uuid_query() . 1409 | '; 1410 | ' . 1411 | array_reduce($this->get_columns($table__value), function ($carry, $column) use ( 1412 | $table__value, 1413 | $primary_key 1414 | ) { 1415 | if ( 1416 | $column === $primary_key || 1417 | $column === 'updated_by' || 1418 | $column === 'created_by' || 1419 | $column === 'created_at' || 1420 | $column === 'updated_at' || 1421 | (isset($this->config['exclude']) && 1422 | isset($this->config['exclude']['columns']) && 1423 | isset($this->config['exclude']['columns'][$table__value]) && 1424 | in_array($column, $this->config['exclude']['columns'][$table__value])) 1425 | ) { 1426 | return $carry; 1427 | } 1428 | 1429 | $carry .= 1430 | ' 1431 | INSERT INTO ' . 1432 | $this->config['logging_table'] . 1433 | '(log_event,log_table,log_key,log_column,log_value,log_uuid,updated_by) 1434 | VALUES(\'insert\', \'' . 1435 | $table__value . 1436 | '\', NEW.' . 1437 | $this->quote($primary_key) . 1438 | ', \'' . 1439 | $column . 1440 | '\', NEW.' . 1441 | $this->quote($column) . 1442 | ', @uuid, NEW.updated_by); 1443 | '; 1444 | return $carry; 1445 | }) . 1446 | ' 1447 | END 1448 | '; 1449 | 1450 | $this->query($query); 1451 | 1452 | $query = 1453 | ' 1454 | CREATE TRIGGER ' . 1455 | $this->quote('trigger-logging-update-' . $table__value) . 1456 | ' 1457 | AFTER UPDATE ON ' . 1458 | $table__value . 1459 | ' FOR EACH ROW 1460 | BEGIN 1461 | DECLARE uuid TEXT; 1462 | SET @uuid := ' . 1463 | $this->uuid_query() . 1464 | '; 1465 | ' . 1466 | array_reduce($this->get_columns($table__value), function ($carry, $column) use ( 1467 | $table__value, 1468 | $primary_key 1469 | ) { 1470 | if ( 1471 | $column === $primary_key || 1472 | $column === 'updated_by' || 1473 | $column === 'created_by' || 1474 | $column === 'created_at' || 1475 | $column === 'updated_at' || 1476 | (isset($this->config['exclude']) && 1477 | isset($this->config['exclude']['columns']) && 1478 | isset($this->config['exclude']['columns'][$table__value]) && 1479 | in_array($column, $this->config['exclude']['columns'][$table__value])) 1480 | ) { 1481 | return $carry; 1482 | } 1483 | $carry .= 1484 | ' 1485 | IF (OLD.' . 1486 | $this->quote($column) . 1487 | ' <> NEW.' . 1488 | $this->quote($column) . 1489 | ') OR (OLD.' . 1490 | $this->quote($column) . 1491 | ' IS NULL AND NEW.' . 1492 | $this->quote($column) . 1493 | ' IS NOT NULL) OR (OLD.' . 1494 | $this->quote($column) . 1495 | ' IS NOT NULL AND NEW.' . 1496 | $this->quote($column) . 1497 | ' IS NULL) THEN 1498 | INSERT INTO ' . 1499 | $this->config['logging_table'] . 1500 | '(log_event,log_table,log_key,log_column,log_value,log_uuid,updated_by) 1501 | VALUES(\'update\', \'' . 1502 | $table__value . 1503 | '\', NEW.' . 1504 | $this->quote($primary_key) . 1505 | ', \'' . 1506 | $column . 1507 | '\', NEW.' . 1508 | $this->quote($column) . 1509 | ', @uuid, NEW.updated_by); 1510 | END IF; 1511 | '; 1512 | return $carry; 1513 | }) . 1514 | ' 1515 | END 1516 | '; 1517 | 1518 | $this->query($query); 1519 | 1520 | $query = 1521 | ' 1522 | CREATE TRIGGER ' . 1523 | $this->quote('trigger-logging-delete-' . $table__value) . 1524 | ' 1525 | AFTER DELETE ON ' . 1526 | $table__value . 1527 | ' FOR EACH ROW 1528 | BEGIN 1529 | DECLARE uuid TEXT; 1530 | SET @uuid := ' . 1531 | $this->uuid_query() . 1532 | '; 1533 | IF( NOT EXISTS( SELECT * FROM ' . 1534 | $this->config['logging_table'] . 1535 | ' WHERE log_event = \'delete\' AND log_table = \'' . 1536 | $table__value . 1537 | '\' AND log_key = OLD.' . 1538 | $this->quote($primary_key) . 1539 | ' ) ) THEN 1540 | INSERT INTO ' . 1541 | $this->config['logging_table'] . 1542 | '(log_event,log_table,log_key,log_column,log_value,log_uuid,updated_by) 1543 | VALUES(\'delete\', \'' . 1544 | $table__value . 1545 | '\', OLD.' . 1546 | $this->quote($primary_key) . 1547 | ', NULL, NULL, @uuid, OLD.updated_by); 1548 | END IF; 1549 | END 1550 | '; 1551 | 1552 | $this->query($query); 1553 | } 1554 | } 1555 | 1556 | private function setup_logging_create_triggers_postgres() 1557 | { 1558 | foreach ($this->get_tables() as $table__value) { 1559 | $this->query( 1560 | 'DROP TRIGGER IF EXISTS ' . 1561 | $this->quote('trigger-logging-insert-' . $table__value) . 1562 | ' ON ' . 1563 | $this->quote($table__value) 1564 | ); 1565 | $this->query( 1566 | 'DROP TRIGGER IF EXISTS ' . 1567 | $this->quote('trigger-logging-update-' . $table__value) . 1568 | ' ON ' . 1569 | $this->quote($table__value) 1570 | ); 1571 | $this->query( 1572 | 'DROP TRIGGER IF EXISTS ' . 1573 | $this->quote('trigger-logging-delete-' . $table__value) . 1574 | ' ON ' . 1575 | $this->quote($table__value) 1576 | ); 1577 | 1578 | if ( 1579 | isset($this->config['exclude']) && 1580 | isset($this->config['exclude']['tables']) && 1581 | in_array($table__value, $this->config['exclude']['tables']) 1582 | ) { 1583 | continue; 1584 | } 1585 | if ($table__value === $this->config['logging_table']) { 1586 | continue; 1587 | } 1588 | 1589 | $primary_key = $this->get_primary_key($table__value); 1590 | 1591 | $query = 1592 | ' 1593 | CREATE OR REPLACE FUNCTION trigger_logging_insert_' . 1594 | $table__value . 1595 | '() 1596 | RETURNS TRIGGER AS $$ 1597 | DECLARE 1598 | uuid TEXT; 1599 | BEGIN 1600 | uuid := ' . 1601 | $this->uuid_query() . 1602 | '; 1603 | ' . 1604 | array_reduce($this->get_columns($table__value), function ($carry, $column) use ( 1605 | $table__value, 1606 | $primary_key 1607 | ) { 1608 | if ( 1609 | $column === $primary_key || 1610 | $column === 'updated_by' || 1611 | $column === 'created_by' || 1612 | $column === 'created_at' || 1613 | $column === 'updated_at' || 1614 | (isset($this->config['exclude']) && 1615 | isset($this->config['exclude']['columns']) && 1616 | isset($this->config['exclude']['columns'][$table__value]) && 1617 | in_array($column, $this->config['exclude']['columns'][$table__value])) 1618 | ) { 1619 | return $carry; 1620 | } 1621 | 1622 | $carry .= 1623 | ' 1624 | INSERT INTO ' . 1625 | $this->config['logging_table'] . 1626 | '(log_event,log_table,log_key,log_column,log_value,log_uuid,updated_by) 1627 | VALUES(\'insert\', \'' . 1628 | $table__value . 1629 | '\', NEW.' . 1630 | $this->quote($primary_key) . 1631 | '::text, \'' . 1632 | $column . 1633 | '\', NEW.' . 1634 | $this->quote($column) . 1635 | '::text, uuid, NEW.updated_by); 1636 | '; 1637 | return $carry; 1638 | }) . 1639 | ' 1640 | RETURN NULL; 1641 | END; 1642 | $$ language \'plpgsql\'; 1643 | '; 1644 | $this->query($query); 1645 | $query = 1646 | ' 1647 | CREATE TRIGGER ' . 1648 | $this->quote('trigger-logging-insert-' . $table__value) . 1649 | ' 1650 | AFTER INSERT ON ' . 1651 | $table__value . 1652 | ' FOR EACH ROW 1653 | EXECUTE PROCEDURE trigger_logging_insert_' . 1654 | $table__value . 1655 | '(); 1656 | '; 1657 | $this->query($query); 1658 | 1659 | $query = 1660 | ' 1661 | CREATE OR REPLACE FUNCTION trigger_logging_update_' . 1662 | $table__value . 1663 | '() 1664 | RETURNS TRIGGER AS $$ 1665 | DECLARE 1666 | uuid TEXT; 1667 | BEGIN 1668 | uuid := ' . 1669 | $this->uuid_query() . 1670 | '; 1671 | ' . 1672 | array_reduce($this->get_columns($table__value), function ($carry, $column) use ( 1673 | $table__value, 1674 | $primary_key 1675 | ) { 1676 | if ( 1677 | $column === $primary_key || 1678 | $column === 'updated_by' || 1679 | $column === 'created_by' || 1680 | $column === 'created_at' || 1681 | $column === 'updated_at' || 1682 | (isset($this->config['exclude']) && 1683 | isset($this->config['exclude']['columns']) && 1684 | isset($this->config['exclude']['columns'][$table__value]) && 1685 | in_array($column, $this->config['exclude']['columns'][$table__value])) 1686 | ) { 1687 | return $carry; 1688 | } 1689 | $carry .= 1690 | ' 1691 | IF (OLD.' . 1692 | $this->quote($column) . 1693 | ' <> NEW.' . 1694 | $this->quote($column) . 1695 | ') OR (OLD.' . 1696 | $this->quote($column) . 1697 | ' IS NULL AND NEW.' . 1698 | $this->quote($column) . 1699 | ' IS NOT NULL) OR (OLD.' . 1700 | $this->quote($column) . 1701 | ' IS NOT NULL AND NEW.' . 1702 | $this->quote($column) . 1703 | ' IS NULL) THEN 1704 | INSERT INTO ' . 1705 | $this->config['logging_table'] . 1706 | '(log_event,log_table,log_key,log_column,log_value,log_uuid,updated_by) 1707 | VALUES(\'update\', \'' . 1708 | $table__value . 1709 | '\', NEW.' . 1710 | $this->quote($primary_key) . 1711 | '::text, \'' . 1712 | $column . 1713 | '\', NEW.' . 1714 | $this->quote($column) . 1715 | '::text, uuid, NEW.updated_by); 1716 | END IF; 1717 | '; 1718 | return $carry; 1719 | }) . 1720 | ' 1721 | RETURN NULL; 1722 | END; 1723 | $$ language \'plpgsql\'; 1724 | '; 1725 | $this->query($query); 1726 | $query = 1727 | ' 1728 | CREATE TRIGGER ' . 1729 | $this->quote('trigger-logging-update-' . $table__value) . 1730 | ' 1731 | AFTER UPDATE ON ' . 1732 | $table__value . 1733 | ' FOR EACH ROW 1734 | EXECUTE PROCEDURE trigger_logging_update_' . 1735 | $table__value . 1736 | '(); 1737 | '; 1738 | $this->query($query); 1739 | 1740 | $query = 1741 | ' 1742 | CREATE OR REPLACE FUNCTION trigger_logging_delete_' . 1743 | $table__value . 1744 | '() 1745 | RETURNS TRIGGER AS $$ 1746 | DECLARE 1747 | uuid TEXT; 1748 | BEGIN 1749 | uuid := ' . 1750 | $this->uuid_query() . 1751 | '; 1752 | IF( NOT EXISTS( SELECT * FROM ' . 1753 | $this->config['logging_table'] . 1754 | ' WHERE log_event = \'delete\' AND log_table = \'' . 1755 | $table__value . 1756 | '\' AND log_key = OLD.' . 1757 | $this->quote($primary_key) . 1758 | '::text ) ) THEN 1759 | INSERT INTO ' . 1760 | $this->config['logging_table'] . 1761 | '(log_event,log_table,log_key,log_column,log_value,log_uuid,updated_by) 1762 | VALUES(\'delete\', \'' . 1763 | $table__value . 1764 | '\', OLD.' . 1765 | $this->quote($primary_key) . 1766 | '::text, NULL, NULL, uuid, OLD.updated_by); 1767 | END IF; 1768 | RETURN NULL; 1769 | END; 1770 | $$ language \'plpgsql\'; 1771 | '; 1772 | $this->query($query); 1773 | $query = 1774 | ' 1775 | CREATE TRIGGER ' . 1776 | $this->quote('trigger-logging-delete-' . $table__value) . 1777 | ' 1778 | AFTER DELETE ON ' . 1779 | $table__value . 1780 | ' FOR EACH ROW 1781 | EXECUTE PROCEDURE trigger_logging_delete_' . 1782 | $table__value . 1783 | '(); 1784 | '; 1785 | $this->query($query); 1786 | } 1787 | } 1788 | 1789 | private function preparse_query($query, $params) 1790 | { 1791 | $return = $query; 1792 | 1793 | /* 1794 | expand IN-syntax 1795 | fetch('SELECT * FROM table WHERE col1 IN (?) AND col2 IN (?) AND col3 IN (?,?) col4 IN (?)', [1], 2, [7,8], [3,4,5]) 1796 | gets to 1797 | fetch('SELECT * FROM table WHERE col1 IN (?) AND col2 IN (?) AND col4 IN (?,?) AND col4 IN (?,?,?)', 1, 2, 3, 7, 8, 3, 4, 5) 1798 | */ 1799 | if (strpos($query, 'IN (') !== false || strpos($query, 'IN(') !== false) { 1800 | if (!empty($params)) { 1801 | $in_index = 0; 1802 | foreach ($params as $params__key => $params__value) { 1803 | if ( 1804 | is_array($params__value) && 1805 | count($params__value) > 0 && 1806 | ((count($params) === 1 && substr_count($query, '?') === 1) || count($params) >= 2) 1807 | ) { 1808 | $in_occurence = $this->find_nth_occurence($return, '?', $in_index); 1809 | if (substr($return, $in_occurence - 1, 3) == '(?)') { 1810 | $return = 1811 | substr($return, 0, $in_occurence - 1) . 1812 | '(' . 1813 | (str_repeat('?,', count($params__value) - 1) . '?') . 1814 | ')' . 1815 | substr($return, $in_occurence + 2); 1816 | } 1817 | foreach ($params__value as $params__value__value) { 1818 | $in_index++; 1819 | } 1820 | } else { 1821 | $in_index++; 1822 | } 1823 | } 1824 | } 1825 | } 1826 | 1827 | /* 1828 | finally flatten all arguments 1829 | example: 1830 | fetch('SELECT * FROM table WHERE ID = ?', [1], 2, [3], [4,5,6]) 1831 | => 1832 | fetch('SELECT * FROM table WHERE ID = ?', 1, 2, 3, 4, 5, 6) 1833 | */ 1834 | if (!empty($params)) { 1835 | $params_flattened = []; 1836 | array_walk_recursive($params, function ($a) use (&$params_flattened) { 1837 | $params_flattened[] = $a; 1838 | }); 1839 | $params = $params_flattened; 1840 | } 1841 | 1842 | // try to sort out bad queries 1843 | foreach ($params as $params__key => $params__value) { 1844 | if (is_object($params__value)) { 1845 | throw new \Exception('object in query'); 1846 | } 1847 | } 1848 | 1849 | // NULL values are treated specially: modify the query 1850 | $pos = 0; 1851 | $delete_keys = []; 1852 | foreach ($params as $params__key => $params__value) { 1853 | // no more ?s are left 1854 | if (($pos = strpos($return, '?', $pos + 1)) === false) { 1855 | break; 1856 | } 1857 | 1858 | // if param is not null, nothing must be done 1859 | if (!is_null($params__value)) { 1860 | continue; 1861 | } 1862 | 1863 | // case 1: if query contains WHERE before ?, then convert != ? to IS NOT NULL and = ? to IS NULL 1864 | if (strpos(substr($return, 0, $pos), 'WHERE') !== false) { 1865 | if (strpos(substr($return, $pos - 5, 6), '<> ?') !== false) { 1866 | $return = 1867 | substr($return, 0, $pos - 5) . 1868 | preg_replace('/<> \?/', 'IS NOT NULL', substr($return, $pos - 5), 1); 1869 | } elseif (strpos(substr($return, $pos - 5, 6), '!= ?') !== false) { 1870 | $return = 1871 | substr($return, 0, $pos - 5) . 1872 | preg_replace('/\!= \?/', 'IS NOT NULL', substr($return, $pos - 5), 1); 1873 | } elseif (strpos(substr($return, $pos - 5, 6), '= ?') !== false) { 1874 | $return = 1875 | substr($return, 0, $pos - 5) . preg_replace('/= \?/', 'IS NULL', substr($return, $pos - 5), 1); 1876 | } 1877 | } 1878 | // case 2: in all other cases, convert ? to NULL 1879 | else { 1880 | $return = substr($return, 0, $pos) . 'NULL' . substr($return, $pos + 1); 1881 | } 1882 | 1883 | // delete param 1884 | $delete_keys[] = $params__key; 1885 | } 1886 | if (!empty($delete_keys)) { 1887 | foreach ($delete_keys as $delete_keys__value) { 1888 | unset($params[$delete_keys__value]); 1889 | } 1890 | } 1891 | $params = array_values($params); 1892 | 1893 | // WordPress: replace ? with %s 1894 | if ($this->connect->driver == 'wordpress') { 1895 | foreach ($params as $params__key => $params__value) { 1896 | // replace next occurence 1897 | if (strpos($return, '?') !== false) { 1898 | $directive = '%s'; 1899 | if ( 1900 | (is_int($params__value) || ctype_digit((string) $params__value)) && 1901 | // prevent strings like "00001" to be catched as integers 1902 | ((strlen($params__value) === 1 && $params__value == '0') || strpos($params__value, '0') !== 0) 1903 | ) { 1904 | $directive = '%d'; 1905 | } elseif (is_float($params__value)) { 1906 | $directive = '%f'; 1907 | } 1908 | $return = substr_replace($return, $directive, strpos($return, '?'), strlen('?')); 1909 | } 1910 | } 1911 | } 1912 | 1913 | // WordPress: pass stripslashes_deep to all parameters (wordpress always adds slashes to them) 1914 | if ($this->connect->driver == 'wordpress') { 1915 | $params = stripslashes_deep($params); 1916 | } 1917 | 1918 | // trim final result 1919 | $return = trim($return); 1920 | 1921 | return [$return, $params]; 1922 | } 1923 | 1924 | public function debug($query) 1925 | { 1926 | $params = func_get_args(); 1927 | unset($params[0]); 1928 | $params = array_values($params); 1929 | [$query, $params] = $this->preparse_query($query, $params); 1930 | 1931 | $keys = []; 1932 | $values = $params; 1933 | foreach ($params as $key => $value) { 1934 | // check if named parameters (':param') or anonymous parameters ('?') are used 1935 | if (is_string($key)) { 1936 | $keys[] = '/:' . $key . '/'; 1937 | } else { 1938 | $keys[] = '/[?]/'; 1939 | } 1940 | // bring parameter into human-readable format 1941 | if (is_string($value)) { 1942 | $values[$key] = "'" . $value . "'"; 1943 | } elseif (is_array($value)) { 1944 | $values[$key] = implode(',', $value); 1945 | } elseif (is_null($value)) { 1946 | $values[$key] = 'NULL'; 1947 | } 1948 | } 1949 | $query = preg_replace($keys, $values, $query, 1, $count); 1950 | return $query; 1951 | } 1952 | 1953 | private function find_occurences($haystack, $needle) 1954 | { 1955 | $positions = []; 1956 | $pos_last = 0; 1957 | while (($pos_last = strpos($haystack, $needle, $pos_last)) !== false) { 1958 | $positions[] = $pos_last; 1959 | $pos_last = $pos_last + strlen($needle); 1960 | } 1961 | return $positions; 1962 | } 1963 | 1964 | private function find_nth_occurence($haystack, $needle, $index) 1965 | { 1966 | $positions = $this->find_occurences($haystack, $needle); 1967 | if (empty($positions) || $index > count($positions) - 1) { 1968 | return null; 1969 | } 1970 | return $positions[$index]; 1971 | } 1972 | 1973 | private function update_batch($table, $input) 1974 | { 1975 | $query = ''; 1976 | $args = []; 1977 | $query = 'UPDATE ' . $table . ' SET' . PHP_EOL; 1978 | foreach ($input[0][0] as $col__key => $col__value) { 1979 | $query .= $col__key . ' = CASE' . PHP_EOL; 1980 | foreach ($input as $input__key => $input__value) { 1981 | $query .= 'WHEN ('; 1982 | $where = []; 1983 | foreach ($input__value[1] as $where__key => $where__value) { 1984 | $where[] = $where__key . ' = ?'; 1985 | $args[] = $where__value; 1986 | } 1987 | $query .= implode(' AND ', $where) . ')' . ' THEN ?' . PHP_EOL; 1988 | $args[] = $input__value[0][$col__key]; 1989 | } 1990 | $query .= 'END'; 1991 | if (array_keys($input[0][0])[count(array_keys($input[0][0])) - 1] !== $col__key) { 1992 | $query .= ','; 1993 | } 1994 | $query .= PHP_EOL; 1995 | } 1996 | $query .= 'WHERE '; 1997 | $where = []; 1998 | foreach ($input[0][1] as $where__key => $where__value) { 1999 | $where_values = []; 2000 | foreach ($input as $input__key => $input__value) { 2001 | $where_values[] = $input__value[1][$where__key]; 2002 | } 2003 | $where_values = array_unique($where_values); 2004 | $where[] = $where__key . ' IN (' . str_repeat('?,', count($where_values) - 1) . '?)'; 2005 | $args = array_merge($args, $where_values); 2006 | } 2007 | $query .= implode(' AND ', $where) . ';'; 2008 | array_unshift($args, $query); 2009 | return call_user_func_array([$this, 'query'], $args); 2010 | } 2011 | 2012 | private function handle_logging($query, $params) 2013 | { 2014 | $table = $this->get_table_name_from_query($query); 2015 | 2016 | if ( 2017 | isset($this->config['exclude']) && 2018 | isset($this->config['exclude']['tables']) && 2019 | in_array($table, $this->config['exclude']['tables']) 2020 | ) { 2021 | return $query; 2022 | } 2023 | if ($table === $this->config['logging_table']) { 2024 | return $query; 2025 | } 2026 | 2027 | if (stripos($query, 'INSERT') === 0) { 2028 | $pos1 = strpos($query, ')'); 2029 | $pos2 = strrpos($query, ')'); 2030 | $pos3 = stripos($query, 'INTO') + strlen('INTO'); 2031 | $pos4 = strpos($query, '('); 2032 | if ( 2033 | $pos1 === false || 2034 | $pos2 === false || 2035 | $pos2 != strlen($query) - 1 || 2036 | strpos(substr($query, 0, $pos1), 'updated_by') !== false 2037 | ) { 2038 | return $query; 2039 | } 2040 | $query = 2041 | substr($query, 0, $pos1) . 2042 | ',updated_by' . 2043 | substr($query, $pos1, $pos2 - $pos1) . 2044 | ',\'' . 2045 | $this->config['updated_by'] . 2046 | '\'' . 2047 | substr($query, $pos2); 2048 | } elseif (stripos($query, 'UPDATE') === 0) { 2049 | $pos1 = stripos($query, 'SET') + strlen('SET'); 2050 | if ($pos1 !== false && strpos(substr($query, $pos1), 'updated_by') === false) { 2051 | $query = 2052 | substr($query, 0, $pos1) . 2053 | ' updated_by = \'' . 2054 | $this->config['updated_by'] . 2055 | '\', ' . 2056 | substr($query, $pos1); 2057 | } 2058 | } elseif (stripos($query, 'DELETE') === 0) { 2059 | // fetch all ids that are affected 2060 | $ids = $this->fetch_col( 2061 | 'SELECT ' . $this->get_primary_key($table) . ' ' . substr($query, stripos($query, 'FROM')), 2062 | $params 2063 | ); 2064 | if (!empty($ids)) { 2065 | foreach ($ids as $id) { 2066 | $this->insert('logs', [ 2067 | 'log_event' => 'delete', 2068 | 'log_table' => $table, 2069 | 'log_key' => $id, 2070 | 'log_uuid' => $this->uuid(), 2071 | 'updated_by' => $this->config['updated_by'] 2072 | ]); 2073 | } 2074 | } 2075 | } 2076 | 2077 | return $query; 2078 | } 2079 | 2080 | private function get_table_name_from_query($query) 2081 | { 2082 | $table = ''; 2083 | 2084 | if (stripos($query, 'INSERT') === 0) { 2085 | $pos1 = stripos($query, 'INTO') + strlen('INTO'); 2086 | $pos2 = strpos($query, '('); 2087 | $table = substr($query, $pos1, $pos2 - $pos1); 2088 | } elseif (stripos($query, 'UPDATE') === 0) { 2089 | $pos1 = stripos($query, 'UPDATE') + strlen('UPDATE'); 2090 | $pos2 = stripos($query, 'SET'); 2091 | $table = substr($query, $pos1, $pos2 - $pos1); 2092 | } elseif (stripos($query, 'DELETE') === 0) { 2093 | $pos1 = stripos($query, 'FROM') + strlen('FROM'); 2094 | $pos2 = stripos($query, 'WHERE'); 2095 | if ($pos2 === false) { 2096 | $pos2 = strlen($query); 2097 | } 2098 | $table = substr($query, $pos1, $pos2 - $pos1); 2099 | } 2100 | 2101 | $table = str_replace('`', '', $table); 2102 | $table = str_replace('"', '', $table); 2103 | $table = trim($table); 2104 | 2105 | return $table; 2106 | } 2107 | 2108 | public function quote($name) 2109 | { 2110 | if ($this->connect->engine === 'mysql') { 2111 | return '`' . $name . '`'; 2112 | } 2113 | if ($this->connect->engine === 'postgres') { 2114 | return '"' . $name . '"'; 2115 | } 2116 | if ($this->connect->engine === 'sqlite') { 2117 | return '"' . $name . '"'; 2118 | } 2119 | } 2120 | 2121 | public function uuid() 2122 | { 2123 | return $this->fetch_var('SELECT ' . $this->uuid_query()); 2124 | } 2125 | 2126 | private function uuid_query() 2127 | { 2128 | if ($this->connect->engine === 'mysql') { 2129 | return 'UUID()'; 2130 | } 2131 | if ($this->connect->engine === 'postgres') { 2132 | return 'uuid_in(md5(random()::text || now()::text)::cstring)'; 2133 | } 2134 | if ($this->connect->engine === 'sqlite') { 2135 | return "substr(u,1,8)||'-'||substr(u,9,4)||'-4'||substr(u,13,3)||'-'||v||substr(u,17,3)||'-'||substr(u,21,12) from (select lower(hex(randomblob(16))) as u, substr('89ab',abs(random()) % 4 + 1, 1) as v)"; 2136 | } 2137 | } 2138 | } 2139 | --------------------------------------------------------------------------------