questlab/www/analytics/core/Db/BatchInsert.php
coderkun 046a724272 merge
2015-04-27 16:42:05 +02:00

255 lines
9.1 KiB
PHP

<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Db;
use Exception;
use Piwik\AssetManager;
use Piwik\Common;
use Piwik\Config;
use Piwik\Db;
use Piwik\DbHelper;
use Piwik\Log;
use Piwik\SettingsPiwik;
use Piwik\SettingsServer;
class BatchInsert
{
/**
* Performs a batch insert into a specific table by iterating through the data
*
* NOTE: you should use tableInsertBatch() which will fallback to this function if LOAD DATA INFILE not available
*
* @param string $tableName PREFIXED table name! you must call Common::prefixTable() before passing the table name
* @param array $fields array of unquoted field names
* @param array $values array of data to be inserted
* @param bool $ignoreWhenDuplicate Ignore new rows that contain unique key values that duplicate old rows
*/
public static function tableInsertBatchIterate($tableName, $fields, $values, $ignoreWhenDuplicate = true)
{
$fieldList = '(' . join(',', $fields) . ')';
$ignore = $ignoreWhenDuplicate ? 'IGNORE' : '';
foreach ($values as $row) {
$query = "INSERT $ignore
INTO " . $tableName . "
$fieldList
VALUES (" . Common::getSqlStringFieldsArray($row) . ")";
Db::query($query, $row);
}
}
/**
* Performs a batch insert into a specific table using either LOAD DATA INFILE or plain INSERTs,
* as a fallback. On MySQL, LOAD DATA INFILE is 20x faster than a series of plain INSERTs.
*
* @param string $tableName PREFIXED table name! you must call Common::prefixTable() before passing the table name
* @param array $fields array of unquoted field names
* @param array $values array of data to be inserted
* @param bool $throwException Whether to throw an exception that was caught while trying
* LOAD DATA INFILE, or not.
* @throws Exception
* @return bool True if the bulk LOAD was used, false if we fallback to plain INSERTs
*/
public static function tableInsertBatch($tableName, $fields, $values, $throwException = false)
{
$filePath = PIWIK_USER_PATH . '/tmp/assets/' . $tableName . '-' . Common::generateUniqId() . '.csv';
$filePath = SettingsPiwik::rewriteTmpPathWithHostname($filePath);
$loadDataInfileEnabled = Config::getInstance()->General['enable_load_data_infile'];
if ($loadDataInfileEnabled
&& Db::get()->hasBulkLoader()) {
try {
$fileSpec = array(
'delim' => "\t",
'quote' => '"', // chr(34)
'escape' => '\\\\', // chr(92)
'escapespecial_cb' => function ($str) {
return str_replace(array(chr(92), chr(34)), array(chr(92) . chr(92), chr(92) . chr(34)), $str);
},
'eol' => "\r\n",
'null' => 'NULL',
);
// hack for charset mismatch
if (!DbHelper::isDatabaseConnectionUTF8() && !isset(Config::getInstance()->database['charset'])) {
$fileSpec['charset'] = 'latin1';
}
self::createCSVFile($filePath, $fileSpec, $values);
if (!is_readable($filePath)) {
throw new Exception("File $filePath could not be read.");
}
$rc = self::createTableFromCSVFile($tableName, $fields, $filePath, $fileSpec);
if ($rc) {
unlink($filePath);
return true;
}
} catch (Exception $e) {
Log::info("LOAD DATA INFILE failed or not supported, falling back to normal INSERTs... Error was: %s", $e->getMessage());
if ($throwException) {
throw $e;
}
}
}
// if all else fails, fallback to a series of INSERTs
@unlink($filePath);
self::tableInsertBatchIterate($tableName, $fields, $values);
return false;
}
/**
* Batch insert into table from CSV (or other delimited) file.
*
* @param string $tableName Name of table
* @param array $fields Field names
* @param string $filePath Path name of a file.
* @param array $fileSpec File specifications (delimiter, line terminator, etc)
*
* @throws Exception
* @return bool True if successful; false otherwise
*/
public static function createTableFromCSVFile($tableName, $fields, $filePath, $fileSpec)
{
// Chroot environment: prefix the path with the absolute chroot path
$chrootPath = Config::getInstance()->General['absolute_chroot_path'];
if(!empty($chrootPath)) {
$filePath = $chrootPath . $filePath;
}
// On Windows, MySQL expects forward slashes as directory separators
if (SettingsServer::isWindows()) {
$filePath = str_replace('\\', '/', $filePath);
}
$query = "
'$filePath'
REPLACE
INTO TABLE
`" . $tableName . "`";
if (isset($fileSpec['charset'])) {
$query .= ' CHARACTER SET ' . $fileSpec['charset'];
}
$fieldList = '(' . join(',', $fields) . ')';
$query .= "
FIELDS TERMINATED BY
'" . $fileSpec['delim'] . "'
ENCLOSED BY
'" . $fileSpec['quote'] . "'
";
if (isset($fileSpec['escape'])) {
$query .= " ESCAPED BY '" . $fileSpec['escape'] . "'";
}
$query .= "
LINES TERMINATED BY
'" . $fileSpec['eol'] . "'
$fieldList
";
/*
* First attempt: assume web server and MySQL server are on the same machine;
* this requires that the db user have the FILE privilege; however, since this is
* a global privilege, it may not be granted due to security concerns
*/
$keywords = array('');
/*
* Second attempt: using the LOCAL keyword means the client reads the file and sends it to the server;
* the LOCAL keyword may trigger a known PHP PDO_MYSQL bug when MySQL not built with --enable-local-infile
* @see http://bugs.php.net/bug.php?id=54158
*/
$openBaseDir = ini_get('open_basedir');
$safeMode = ini_get('safe_mode');
if (empty($openBaseDir) && empty($safeMode)) {
// php 5.x - LOAD DATA LOCAL INFILE is disabled if open_basedir restrictions or safe_mode enabled
$keywords[] = 'LOCAL ';
}
$exceptions = array();
foreach ($keywords as $keyword) {
$queryStart = 'LOAD DATA ' . $keyword . 'INFILE ';
$sql = $queryStart . $query;
try {
$result = @Db::exec($sql);
if (empty($result) || $result < 0) {
continue;
}
return true;
} catch (Exception $e) {
// echo $sql . ' ---- ' . $e->getMessage();
$code = $e->getCode();
$message = $e->getMessage() . ($code ? "[$code]" : '');
if (!Db::get()->isErrNo($e, '1148')) {
Log::info("LOAD DATA INFILE failed... Error was: %s", $message);
}
$exceptions[] = "\n Try #" . (count($exceptions) + 1) . ': ' . $queryStart . ": " . $message;
}
}
if (count($exceptions)) {
throw new Exception(implode(",", $exceptions));
}
return false;
}
/**
* Create CSV (or other delimited) files
*
* @param string $filePath filename to create
* @param array $fileSpec File specifications (delimiter, line terminator, etc)
* @param array $rows Array of array corresponding to rows of values
* @throws Exception if unable to create or write to file
*/
static protected function createCSVFile($filePath, $fileSpec, $rows)
{
// Set up CSV delimiters, quotes, etc
$delim = $fileSpec['delim'];
$quote = $fileSpec['quote'];
$eol = $fileSpec['eol'];
$null = $fileSpec['null'];
$escapespecial_cb = $fileSpec['escapespecial_cb'];
$fp = @fopen($filePath, 'wb');
if (!$fp) {
throw new Exception('Error creating the tmp file ' . $filePath . ', please check that the webserver has write permission to write this file.');
}
foreach ($rows as $row) {
$output = '';
foreach ($row as $value) {
if (!isset($value) || is_null($value) || $value === false) {
$output .= $null . $delim;
} else {
$output .= $quote . $escapespecial_cb($value) . $quote . $delim;
}
}
// Replace delim with eol
$output = substr_replace($output, $eol, -1);
$ret = fwrite($fp, $output);
if (!$ret) {
fclose($fp);
throw new Exception('Error writing to the tmp file ' . $filePath);
}
}
fclose($fp);
@chmod($filePath, 0777);
}
}