Adapter
1 month ago
Schema
2 weeks ago
Adapter.php
1 month ago
AdapterInterface.php
1 month ago
BatchInsert.php
6 months ago
Schema.php
1 month ago
SchemaInterface.php
1 month ago
Settings.php
1 year ago
TransactionLevel.php
1 year ago
TransactionalDatabaseDynamicTrait.php
1 year ago
TransactionalDatabaseInterface.php
1 year ago
TransactionalDatabaseStaticTrait.php
1 year ago
BatchInsert.php
267 lines
| 1 | <?php |
| 2 | |
| 3 | /** |
| 4 | * Matomo - free/libre analytics platform |
| 5 | * |
| 6 | * @link https://matomo.org |
| 7 | * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later |
| 8 | */ |
| 9 | namespace Piwik\Db; |
| 10 | |
| 11 | use Exception; |
| 12 | use Piwik\Common; |
| 13 | use Piwik\Config; |
| 14 | use Piwik\Config\DatabaseConfig; |
| 15 | use Piwik\Container\StaticContainer; |
| 16 | use Piwik\Db; |
| 17 | use Piwik\ExceptionHandler; |
| 18 | use Piwik\Log; |
| 19 | use Piwik\SettingsServer; |
| 20 | use Piwik\SettingsPiwik; |
| 21 | class BatchInsert |
| 22 | { |
| 23 | /** |
| 24 | * Performs a batch insert into a specific table by iterating through the data |
| 25 | * |
| 26 | * NOTE: you should use tableInsertBatch() which will fallback to this function if LOAD DATA INFILE not available |
| 27 | * |
| 28 | * @param string $tableName PREFIXED table name! you must call Common::prefixTable() before passing the table name |
| 29 | * @param array $fields array of unquoted field names |
| 30 | * @param array $values array of data to be inserted |
| 31 | * @param bool $ignoreWhenDuplicate Ignore new rows that contain unique key values that duplicate old rows |
| 32 | */ |
| 33 | public static function tableInsertBatchIterate($tableName, $fields, $values, $ignoreWhenDuplicate = \true) |
| 34 | { |
| 35 | $tableName = preg_replace('/[^a-zA-Z\\d_-]/', '', $tableName); |
| 36 | $fieldList = '(' . join(',', $fields) . ')'; |
| 37 | $ignore = $ignoreWhenDuplicate ? 'IGNORE' : ''; |
| 38 | foreach ($values as $row) { |
| 39 | $row = array_values($row); |
| 40 | $query = "INSERT {$ignore} INTO `" . $tableName . "`\n\t\t\t\t\t {$fieldList}\n\t\t\t\t\t VALUES (" . Common::getSqlStringFieldsArray($row) . ")"; |
| 41 | Db::query($query, $row); |
| 42 | } |
| 43 | } |
| 44 | /** |
| 45 | * Performs a batch insert into a specific table by sending all data in one SQL statement. |
| 46 | * |
| 47 | * @param string $tableName PREFIXED table name! you must call Common::prefixTable() before passing the table name |
| 48 | * @param array $fields array of unquoted field names |
| 49 | * @param array $values array of data to be inserted |
| 50 | * @param bool $ignoreWhenDuplicate Ignore new rows that contain unique key values that duplicate old rows |
| 51 | */ |
| 52 | public static function tableInsertBatchSql($tableName, $fields, $values, $ignoreWhenDuplicate = \true) |
| 53 | { |
| 54 | $insertLines = array(); |
| 55 | $bind = array(); |
| 56 | foreach ($values as $row) { |
| 57 | $insertLines[] = "(" . Common::getSqlStringFieldsArray($row) . ")"; |
| 58 | $bind = array_merge($bind, $row); |
| 59 | } |
| 60 | $fieldList = '(' . implode(',', $fields) . ')'; |
| 61 | $insertLines = implode(',', $insertLines); |
| 62 | $ignore = $ignoreWhenDuplicate ? 'IGNORE' : ''; |
| 63 | $query = "INSERT {$ignore} INTO {$tableName} {$fieldList} VALUES {$insertLines}"; |
| 64 | Db::query($query, $bind); |
| 65 | } |
| 66 | /** |
| 67 | * Performs a batch insert into a specific table using either LOAD DATA INFILE or plain INSERTs, |
| 68 | * as a fallback. On MySQL, LOAD DATA INFILE is 20x faster than a series of plain INSERTs. |
| 69 | * |
| 70 | * @param string $tableName PREFIXED table name! you must call Common::prefixTable() before passing the table name |
| 71 | * @param array $fields array of unquoted field names |
| 72 | * @param array $values array of data to be inserted |
| 73 | * @param bool $throwException Whether to throw an exception that was caught while trying |
| 74 | * LOAD DATA INFILE, or not. |
| 75 | * @param string $charset The charset to use, defaults to utf8 |
| 76 | * @throws Exception |
| 77 | * @return bool True if the bulk LOAD was used, false if we fallback to plain INSERTs |
| 78 | */ |
| 79 | public static function tableInsertBatch($tableName, $fields, $values, $throwException = \false, $charset = 'utf8') |
| 80 | { |
| 81 | $loadDataInfileEnabled = Config::getInstance()->General['enable_load_data_infile']; |
| 82 | if ($loadDataInfileEnabled && Db::get()->hasBulkLoader()) { |
| 83 | $path = self::getBestPathForLoadData(); |
| 84 | $instanceId = SettingsPiwik::getPiwikInstanceId(); |
| 85 | if (empty($instanceId)) { |
| 86 | $instanceId = ''; |
| 87 | } |
| 88 | $filePath = $path . $tableName . '-' . $instanceId . Common::generateUniqId() . '.csv'; |
| 89 | // always use utf8 for TiDb, as TiDb has problems with latin1 |
| 90 | if (DatabaseConfig::isTiDb()) { |
| 91 | $charset = 'utf8'; |
| 92 | } |
| 93 | try { |
| 94 | $fileSpec = array( |
| 95 | 'delim' => "\t", |
| 96 | 'quote' => '"', |
| 97 | // chr(34) |
| 98 | 'escape' => '\\\\', |
| 99 | // chr(92) |
| 100 | 'escapespecial_cb' => function ($str) { |
| 101 | return str_replace(array(chr(92), chr(34)), array(chr(92) . chr(92), chr(92) . chr(34)), $str); |
| 102 | }, |
| 103 | 'eol' => "\r\n", |
| 104 | 'null' => 'NULL', |
| 105 | 'charset' => $charset, |
| 106 | ); |
| 107 | self::createCSVFile($filePath, $fileSpec, $values); |
| 108 | if (!is_readable($filePath)) { |
| 109 | throw new Exception("File {$filePath} could not be read."); |
| 110 | } |
| 111 | $rc = self::createTableFromCSVFile($tableName, $fields, $filePath, $fileSpec); |
| 112 | if ($rc) { |
| 113 | unlink($filePath); |
| 114 | return \true; |
| 115 | } |
| 116 | } catch (Exception $e) { |
| 117 | if ($throwException) { |
| 118 | throw $e; |
| 119 | } |
| 120 | } |
| 121 | // if all else fails, fallback to a series of INSERTs |
| 122 | if (file_exists($filePath)) { |
| 123 | @unlink($filePath); |
| 124 | } |
| 125 | } |
| 126 | self::tableInsertBatchIterate($tableName, $fields, $values); |
| 127 | return \false; |
| 128 | } |
| 129 | private static function getBestPathForLoadData() |
| 130 | { |
| 131 | try { |
| 132 | $path = Db::fetchOne('SELECT @@secure_file_priv'); |
| 133 | // was introduced in 5.0.38 |
| 134 | } catch (Exception $e) { |
| 135 | // we do not rethrow exception as an error is expected if MySQL is < 5.0.38 |
| 136 | // in this case tableInsertBatch might still work |
| 137 | } |
| 138 | if (empty($path) || !@is_dir($path) || !@is_writable($path)) { |
| 139 | $path = StaticContainer::get('path.tmp') . '/assets/'; |
| 140 | } elseif (!Common::stringEndsWith($path, '/')) { |
| 141 | $path .= '/'; |
| 142 | } |
| 143 | return $path; |
| 144 | } |
| 145 | /** |
| 146 | * Batch insert into table from CSV (or other delimited) file. |
| 147 | * |
| 148 | * @param string $tableName Name of table |
| 149 | * @param array $fields Field names |
| 150 | * @param string $filePath Path name of a file. |
| 151 | * @param array $fileSpec File specifications (delimiter, line terminator, etc) |
| 152 | * |
| 153 | * @throws Exception |
| 154 | * @return bool True if successful; false otherwise |
| 155 | */ |
| 156 | public static function createTableFromCSVFile($tableName, $fields, $filePath, $fileSpec) |
| 157 | { |
| 158 | // Chroot environment: prefix the path with the absolute chroot path |
| 159 | $chrootPath = Config::getInstance()->General['absolute_chroot_path']; |
| 160 | if (!empty($chrootPath)) { |
| 161 | $filePath = $chrootPath . $filePath; |
| 162 | } |
| 163 | // On Windows, MySQL expects forward slashes as directory separators |
| 164 | if (SettingsServer::isWindows()) { |
| 165 | $filePath = str_replace('\\', '/', $filePath); |
| 166 | } |
| 167 | $query = "\n\t\t\t\t'{$filePath}'\n\t\t\tREPLACE\n\t\t\tINTO TABLE\n\t\t\t\t`" . $tableName . "`"; |
| 168 | if (isset($fileSpec['charset'])) { |
| 169 | $query .= ' CHARACTER SET ' . $fileSpec['charset']; |
| 170 | } |
| 171 | $fieldList = '(' . join(',', $fields) . ')'; |
| 172 | $query .= "\n\t\t\tFIELDS TERMINATED BY\n\t\t\t\t'" . $fileSpec['delim'] . "'\n\t\t\tENCLOSED BY\n\t\t\t\t'" . $fileSpec['quote'] . "'\n\t\t"; |
| 173 | if (isset($fileSpec['escape'])) { |
| 174 | $query .= " ESCAPED BY '" . $fileSpec['escape'] . "'"; |
| 175 | } |
| 176 | $query .= "\n\t\t\tLINES TERMINATED BY\n\t\t\t\t'" . $fileSpec['eol'] . "'\n\t\t\t{$fieldList}\n\t\t"; |
| 177 | /* |
| 178 | * First attempt: assume web server and MySQL server are on the same machine; |
| 179 | * this requires that the db user have the FILE privilege; however, since this is |
| 180 | * a global privilege, it may not be granted due to security concerns |
| 181 | */ |
| 182 | if (Config::getInstance()->General['multi_server_environment']) { |
| 183 | $keywords = array(); |
| 184 | // don't try 'LOAD DATA INFILE' if in a multi_server_environment |
| 185 | } else { |
| 186 | $keywords = array(''); |
| 187 | } |
| 188 | /* |
| 189 | * Second attempt: using the LOCAL keyword means the client reads the file and sends it to the server; |
| 190 | * the LOCAL keyword may trigger a known PHP PDO\MYSQL bug when MySQL not built with --enable-local-infile |
| 191 | * @see https://bugs.php.net/bug.php?id=54158 |
| 192 | */ |
| 193 | $openBaseDir = ini_get('open_basedir'); |
| 194 | $safeMode = ini_get('safe_mode'); |
| 195 | if ((function_exists('mysqli_get_client_stats') || empty($openBaseDir)) && empty($safeMode)) { |
| 196 | // php 5.x - LOAD DATA LOCAL INFILE only used if open_basedir is not set (or we're using a non-buggy version of mysqlnd) |
| 197 | // and if safe mode is not enabled |
| 198 | $keywords[] = 'LOCAL '; |
| 199 | } |
| 200 | $exceptions = array(); |
| 201 | foreach ($keywords as $keyword) { |
| 202 | $queryStart = 'LOAD DATA ' . $keyword . 'INFILE '; |
| 203 | $sql = $queryStart . $query; |
| 204 | try { |
| 205 | $result = @Db::exec($sql); |
| 206 | if (empty($result) || $result < 0) { |
| 207 | continue; |
| 208 | } |
| 209 | return \true; |
| 210 | } catch (Exception $e) { |
| 211 | $code = $e->getCode(); |
| 212 | $message = $e->getMessage() . ($code ? "[{$code}]" : ''); |
| 213 | if (ExceptionHandler::shouldPrintBackTraceWithMessage()) { |
| 214 | $message .= "\n" . $e->getTraceAsString(); |
| 215 | } |
| 216 | $exceptions[] = "\n Try #" . (count($exceptions) + 1) . ': ' . $queryStart . ": " . $message; |
| 217 | } |
| 218 | } |
| 219 | if (count($exceptions)) { |
| 220 | $message = "LOAD DATA INFILE failed... Error was: " . implode(",", $exceptions); |
| 221 | Log::info($message); |
| 222 | throw new Exception($message); |
| 223 | } |
| 224 | return \false; |
| 225 | } |
| 226 | /** |
| 227 | * Create CSV (or other delimited) files |
| 228 | * |
| 229 | * @param string $filePath filename to create |
| 230 | * @param array $fileSpec File specifications (delimiter, line terminator, etc) |
| 231 | * @param array $rows Array of array corresponding to rows of values |
| 232 | * @throws Exception if unable to create or write to file |
| 233 | */ |
| 234 | protected static function createCSVFile($filePath, $fileSpec, $rows) |
| 235 | { |
| 236 | // Set up CSV delimiters, quotes, etc |
| 237 | $delim = $fileSpec['delim']; |
| 238 | $quote = $fileSpec['quote']; |
| 239 | $eol = $fileSpec['eol']; |
| 240 | $null = $fileSpec['null']; |
| 241 | $escapespecial_cb = $fileSpec['escapespecial_cb']; |
| 242 | $fp = @fopen($filePath, 'wb'); |
| 243 | if (!$fp) { |
| 244 | throw new Exception('Error creating the tmp file ' . $filePath . ', please check that the webserver has write permission to write this file.'); |
| 245 | } |
| 246 | foreach ($rows as $row) { |
| 247 | $output = ''; |
| 248 | foreach ($row as $value) { |
| 249 | if (!isset($value) || is_null($value) || $value === \false) { |
| 250 | $output .= $null . $delim; |
| 251 | } else { |
| 252 | $output .= $quote . $escapespecial_cb($value) . $quote . $delim; |
| 253 | } |
| 254 | } |
| 255 | // Replace delim with eol |
| 256 | $output = substr_replace($output, $eol, -1); |
| 257 | $ret = fwrite($fp, $output); |
| 258 | if (!$ret) { |
| 259 | fclose($fp); |
| 260 | throw new Exception('Error writing to the tmp file ' . $filePath); |
| 261 | } |
| 262 | } |
| 263 | fclose($fp); |
| 264 | @chmod($filePath, 0777); |
| 265 | } |
| 266 | } |
| 267 |