cfg = array_combine($this->cfg_keys, $cfg_values); $this->dbg_enabled = (sql_dbg_enabled() || !empty($_COOKIE['explain'])); $this->do_explain = ($this->dbg_enabled && !empty($_COOKIE['explain'])); $this->slow_time = SQL_SLOW_QUERY_TIME; // ссылки на глобальные переменные (для включения логов сразу на всех серверах, подсчета общего количества запросов и т.д.) $this->DBS['log_file'] =& $DBS->log_file; $this->DBS['log_counter'] =& $DBS->log_counter; $this->DBS['num_queries'] =& $DBS->num_queries; $this->DBS['sql_inittime'] =& $DBS->sql_inittime; $this->DBS['sql_timetotal'] =& $DBS->sql_timetotal; } /** * Initialize connection */ public function init() { // Connect to server $this->connect(); // Set charset if ($this->cfg['charset'] && !mysqli_set_charset($this->link, $this->cfg['charset'])) { if (!$this->sql_query("SET NAMES {$this->cfg['charset']}")) { die("Could not set charset {$this->cfg['charset']}"); } } $this->inited = true; $this->num_queries = 0; $this->sql_inittime = $this->sql_timetotal; $this->DBS['sql_inittime'] += $this->sql_inittime; } /** * Open connection */ public function connect() { $this->cur_query = ($this->dbg_enabled) ? "connect to: {$this->cfg['dbhost']}" : 'connect'; $this->debug('start'); $p = ((bool)$this->cfg['persist']) ? 'p:' : ''; $this->link = mysqli_connect($p . $this->cfg['dbhost'], $this->cfg['dbuser'], $this->cfg['dbpasswd'], $this->cfg['dbname']); $this->selected_db = $this->cfg['dbname']; if (mysqli_connect_error()) { $server = (DBG_USER) ? $this->cfg['dbhost'] : ''; header("HTTP/1.0 503 Service Unavailable"); bb_log(' ', "db_err/connect_failed_{$this->cfg['dbhost']}"); die("Could not connect to mysql server $server"); } register_shutdown_function([&$this, 'close']); $this->debug('stop'); $this->cur_query = null; } /** * Base query method * * @param $query * * @return bool|mysqli_result|null */ public function sql_query($query) { if (!$this->link) { $this->init(); } if (is_array($query)) { $query = $this->build_sql($query); } if (SQL_PREPEND_SRC_COMM) { $query = '/* ' . $this->debug_find_source() . ' */ ' . $query; } $this->cur_query = $query; $this->debug('start'); if (!$this->result = mysqli_query($this->link, $query)) { $this->log_error(); } $this->debug('stop'); $this->cur_query = null; if ($this->inited) { $this->num_queries++; $this->DBS['num_queries']++; } return $this->result; } /** * Execute query WRAPPER (with error handling) * * @param $query * * @return bool|mysqli_result|null */ public function query($query) { if (!$result = $this->sql_query($query)) { $this->trigger_error(); } return $result; } /** * Return number of rows * * @param bool $result * * @return bool|int */ public function num_rows($result = false) { $num_rows = false; if ($result or $result = $this->result) { $num_rows = $result instanceof mysqli_result ? mysqli_num_rows($result) : false; } return $num_rows; } /** * Return number of affected rows * * @return int */ public function affected_rows() { return mysqli_affected_rows($this->link); } /** * Fetch current field * * @param $field * @param int $rownum * @param int $query_id * * @return bool */ public function sql_fetchfield($field, $rownum = -1, $query_id = 0) { if (!$query_id) { $query_id = $this->query_result; } if ($query_id) { if ($rownum > -1) { $result = $this->sql_result($query_id, $rownum, $field); } else { if (empty($this->row[$query_id]) && empty($this->rowset[$query_id])) { if ($this->sql_fetchrow()) { $result = $this->row[$query_id][$field]; } } else { if ($this->rowset[$query_id]) { $result = $this->rowset[$query_id][0][$field]; } elseif ($this->row[$query_id]) { $result = $this->row[$query_id][$field]; } } } return $result; } return false; } /** * @param mysqli_result $res * @param $row * @param int $field * * @return mixed */ private function sql_result(mysqli_result $res, $row, $field = 0) { $res->data_seek($row); $dataRow = $res->fetch_array(); return $dataRow[$field]; } /** * Fetch current row * * @param $result * @param string $field_name * * @return array|bool|null */ public function sql_fetchrow($result, $field_name = '') { $row = mysqli_fetch_assoc($result); if ($field_name) { return isset($row[$field_name]) ? $row[$field_name] : false; } return $row; } /** * Alias of sql_fetchrow() * @param $result * * @return array|bool|null */ public function fetch_next($result) { return $this->sql_fetchrow($result); } /** * Fetch row WRAPPER (with error handling) * @param $query * @param string $field_name * * @return array|bool|null */ public function fetch_row($query, $field_name = '') { if (!$result = $this->sql_query($query)) { $this->trigger_error(); } return $this->sql_fetchrow($result, $field_name); } /** * Fetch all rows * * @param $result * @param string $field_name * * @return array */ public function sql_fetchrowset($result, $field_name = '') { $rowset = []; while ($row = mysqli_fetch_assoc($result)) { $rowset[] = ($field_name) ? $row[$field_name] : $row; } return $rowset; } /** * Fetch all rows WRAPPER (with error handling) * * @param $query * @param string $field_name * * @return array */ public function fetch_rowset($query, $field_name = '') { if (!$result = $this->sql_query($query)) { $this->trigger_error(); } return $this->sql_fetchrowset($result, $field_name); } /** * Fetch all rows WRAPPER (with error handling) * * @param $query * @param string $field_name * * @return array */ public function fetch_all($query, $field_name = '') { if (!$result = $this->sql_query($query)) { $this->trigger_error(); } return $this->sql_fetchrowset($result, $field_name); } /** * Get last inserted id after insert statement * * @return int|string */ public function sql_nextid() { return mysqli_insert_id($this->link); } /** * Free sql result * * @param bool $result */ public function sql_freeresult($result = false) { if ($result or $result = $this->result) { if ($result instanceof mysqli_result) { mysqli_free_result($result); } } $this->result = null; } /** * Escape data used in sql query * * @param $v * @param bool $check_type * @param bool $dont_escape * * @return string */ public function escape($v, $check_type = false, $dont_escape = false) { if ($dont_escape) { return $v; } if (!$check_type) { return $this->escape_string($v); } switch (true) { case is_string($v): return "'" . $this->escape_string($v) . "'"; case is_int($v): return "$v"; case is_bool($v): return ($v) ? '1' : '0'; case is_float($v): return "'$v'"; case null === $v: return 'NULL'; } // if $v has unsuitable type $this->trigger_error(__FUNCTION__ . ' - wrong params'); } /** * Escape string * * @param $str * * @return string */ public function escape_string($str) { if (!$this->link) { $this->init(); } return mysqli_real_escape_string($this->link, $str); } /** * Build SQL statement from array. * Possible $query_type values: INSERT, INSERT_SELECT, MULTI_INSERT, UPDATE, SELECT * * @param $query_type * @param $input_ary * @param bool $data_already_escaped * @param bool $check_data_type_in_escape * * @return string */ public function build_array($query_type, $input_ary, $data_already_escaped = false, $check_data_type_in_escape = true) { $fields = $values = $ary = $query = []; $dont_escape = $data_already_escaped; $check_type = $check_data_type_in_escape; if (empty($input_ary) || !is_array($input_ary)) { $this->trigger_error(__FUNCTION__ . ' - wrong params: $input_ary'); } if ($query_type == 'INSERT') { foreach ($input_ary as $field => $val) { $fields[] = $field; $values[] = $this->escape($val, $check_type, $dont_escape); } $fields = implode(', ', $fields); $values = implode(', ', $values); $query = "($fields)\nVALUES\n($values)"; } elseif ($query_type == 'INSERT_SELECT') { foreach ($input_ary as $field => $val) { $fields[] = $field; $values[] = $this->escape($val, $check_type, $dont_escape); } $fields = implode(', ', $fields); $values = implode(', ', $values); $query = "($fields)\nSELECT\n$values"; } elseif ($query_type == 'MULTI_INSERT') { foreach ($input_ary as $id => $sql_ary) { foreach ($sql_ary as $field => $val) { $values[] = $this->escape($val, $check_type, $dont_escape); } $ary[] = '(' . implode(', ', $values) . ')'; $values = []; } $fields = implode(', ', array_keys($input_ary[0])); $values = implode(",\n", $ary); $query = "($fields)\nVALUES\n$values"; } elseif ($query_type == 'SELECT' || $query_type == 'UPDATE') { foreach ($input_ary as $field => $val) { $ary[] = "$field = " . $this->escape($val, $check_type, $dont_escape); } $glue = ($query_type == 'SELECT') ? "\nAND " : ",\n"; $query = implode($glue, $ary); } if (!$query) { bb_die('
' . __FUNCTION__ . ": Wrong params for $query_type query type\n\n\$input_ary:\n\n" . htmlCHR(print_r($input_ary, true)) . ''); } return "\n" . $query . "\n"; } /** * @return array */ public function get_empty_sql_array() { return [ 'SELECT' => [], 'select_options' => [], 'FROM' => [], 'INNER JOIN' => [], 'LEFT JOIN' => [], 'WHERE' => [], 'GROUP BY' => [], 'HAVING' => [], 'ORDER BY' => [], 'LIMIT' => [], ]; } /** * @param $sql_ary * @return string */ public function build_sql($sql_ary) { $sql = ''; array_deep($sql_ary, 'array_unique', false, true); foreach ($sql_ary as $clause => $ary) { switch ($clause) { case 'SELECT': $sql .= ($ary) ? ' SELECT ' . implode(' ', $sql_ary['select_options']) . ' ' . implode(', ', $ary) : ''; break; case 'FROM': $sql .= ($ary) ? ' FROM ' . implode(', ', $ary) : ''; break; case 'INNER JOIN': $sql .= ($ary) ? ' INNER JOIN ' . implode(' INNER JOIN ', $ary) : ''; break; case 'LEFT JOIN': $sql .= ($ary) ? ' LEFT JOIN ' . implode(' LEFT JOIN ', $ary) : ''; break; case 'WHERE': $sql .= ($ary) ? ' WHERE ' . implode(' AND ', $ary) : ''; break; case 'GROUP BY': $sql .= ($ary) ? ' GROUP BY ' . implode(', ', $ary) : ''; break; case 'HAVING': $sql .= ($ary) ? ' HAVING ' . implode(' AND ', $ary) : ''; break; case 'ORDER BY': $sql .= ($ary) ? ' ORDER BY ' . implode(', ', $ary) : ''; break; case 'LIMIT': $sql .= ($ary) ? ' LIMIT ' . implode(', ', $ary) : ''; break; } } return trim($sql); } /** * Return sql error array * * @return array */ public function sql_error() { if ($this->link) { return ['code' => mysqli_errno($this->link), 'message' => mysqli_error($this->link)]; } return ['code' => '', 'message' => 'not connected']; } /** * Close sql connection */ public function close() { if ($this->link) { $this->unlock(); if (!empty($this->locks)) { foreach ($this->locks as $name => $void) { $this->release_lock($name); } } $this->exec_shutdown_queries(); mysqli_close($this->link); } $this->link = $this->selected_db = null; } /** * Add shutdown query * * @param $sql */ public function add_shutdown_query($sql) { $this->shutdown['__sql'][] = $sql; } /** * Exec shutdown queries */ public function exec_shutdown_queries() { if (empty($this->shutdown)) { return; } if (!empty($this->shutdown['post_html'])) { $post_html_sql = $this->build_array('MULTI_INSERT', $this->shutdown['post_html']); $this->query("REPLACE INTO " . BB_POSTS_HTML . " $post_html_sql"); } if (!empty($this->shutdown['__sql'])) { foreach ($this->shutdown['__sql'] as $sql) { $this->query($sql); } } } /** * Lock tables * * @param $tables * @param string $lock_type * * @return bool|mysqli_result|null */ public function lock($tables, $lock_type = 'WRITE') { $tables_sql = []; foreach ((array)$tables as $table_name) { $tables_sql[] = "$table_name $lock_type"; } if ($tables_sql = implode(', ', $tables_sql)) { $this->locked = $this->sql_query("LOCK TABLES $tables_sql"); } return $this->locked; } /** * Unlock tables * * @return bool */ public function unlock() { if ($this->locked && $this->sql_query("UNLOCK TABLES")) { $this->locked = false; } return !$this->locked; } /** * Obtain user level lock * * @param $name * @param int $timeout * * @return mixed */ public function get_lock($name, $timeout = 0) { $lock_name = $this->get_lock_name($name); $timeout = (int)$timeout; $row = $this->fetch_row("SELECT GET_LOCK('$lock_name', $timeout) AS lock_result"); if ($row['lock_result']) { $this->locks[$name] = true; } return $row['lock_result']; } /** * Obtain user level lock status * * @param $name * * @return mixed */ public function release_lock($name) { $lock_name = $this->get_lock_name($name); $row = $this->fetch_row("SELECT RELEASE_LOCK('$lock_name') AS lock_result"); if ($row['lock_result']) { unset($this->locks[$name]); } return $row['lock_result']; } /** * Release user level lock * * @param $name * * @return mixed */ public function is_free_lock($name) { $lock_name = $this->get_lock_name($name); $row = $this->fetch_row("SELECT IS_FREE_LOCK('$lock_name') AS lock_result"); return $row['lock_result']; } /** * Make per db unique lock name * * @param $name * * @return string */ public function get_lock_name($name) { if (!$this->selected_db) { $this->init(); } return "{$this->selected_db}_{$name}"; } /** * Get info about last query * * @return mixed */ public function query_info() { $info = []; if ($num = $this->num_rows($this->result)) { $info[] = "$num rows"; } if ($this->link and $ext = mysqli_info($this->link)) { $info[] = "$ext"; } elseif (!$num && ($aff = $this->affected_rows($this->result) and $aff != -1)) { $info[] = "$aff rows"; } return str_compact(implode(', ', $info)); } /** * Get server version * * @return mixed */ public function server_version() { preg_match('#^(\d+\.\d+\.\d+).*#', mysqli_get_server_info($this->link), $m); return $m[1]; } /** * Set slow query marker for xx seconds. * This will disable counting other queries as "slow" during this time. * * @param int $ignoring_time * @param int $new_priority */ public function expect_slow_query($ignoring_time = 60, $new_priority = 10) { if ($old_priority = CACHE('bb_cache')->get('dont_log_slow_query')) { if ($old_priority > $new_priority) { return; } } @define('IN_FIRST_SLOW_QUERY', true); CACHE('bb_cache')->set('dont_log_slow_query', $new_priority, $ignoring_time); } /** * Store debug info * * @param $mode */ public function debug($mode) { if (!SQL_DEBUG) { return; } $id =& $this->dbg_id; $dbg =& $this->dbg[$id]; if ($mode == 'start') { if (SQL_CALC_QUERY_TIME || DBG_LOG || SQL_LOG_SLOW_QUERIES) { $this->sql_starttime = utime(); } if ($this->dbg_enabled) { $dbg['sql'] = preg_replace('#^(\s*)(/\*)(.*)(\*/)(\s*)#', '', $this->cur_query); $dbg['src'] = $this->debug_find_source(); $dbg['file'] = $this->debug_find_source('file'); $dbg['line'] = $this->debug_find_source('line'); $dbg['time'] = ''; $dbg['info'] = ''; $dbg['mem_before'] = sys('mem'); } if ($this->do_explain) { $this->explain('start'); } } elseif ($mode == 'stop') { if (SQL_CALC_QUERY_TIME || DBG_LOG || SQL_LOG_SLOW_QUERIES) { $this->cur_query_time = utime() - $this->sql_starttime; $this->sql_timetotal += $this->cur_query_time; $this->DBS['sql_timetotal'] += $this->cur_query_time; if (SQL_LOG_SLOW_QUERIES && $this->cur_query_time > $this->slow_time) { $this->log_slow_query(); } } if ($this->dbg_enabled) { $dbg['time'] = utime() - $this->sql_starttime; $dbg['info'] = $this->query_info(); $dbg['mem_after'] = sys('mem'); $id++; } if ($this->do_explain) { $this->explain('stop'); } // проверка установки $this->inited - для пропуска инициализационных запросов if ($this->DBS['log_counter'] && $this->inited) { $this->log_query($this->DBS['log_file']); $this->DBS['log_counter']--; } } } /** * Trigger error * * @param string $msg */ public function trigger_error($msg = 'DB Error') { if (error_reporting()) { if (DBG_LOG === true) { $err = $this->sql_error(); $msg .= "\n" . trim(sprintf('#%06d %s', $err['code'], $err['message'])); } else { $msg .= " [" . $this->debug_find_source() . "]"; } trigger_error($msg, E_USER_ERROR); } } /** * Find caller source * * @param string $mode * * @return string */ public function debug_find_source($mode = '') { foreach (debug_backtrace() as $trace) { if (!empty($trace['file']) && $trace['file'] !== __FILE__) { switch ($mode) { case 'file': return $trace['file']; case 'line': return $trace['line']; default: return hide_bb_path($trace['file']) . '(' . $trace['line'] . ')'; } } } return ''; } /** * Prepare for logging * @param int $queries_count * @param string $log_file */ public function log_next_query($queries_count = 1, $log_file = 'sql_queries') { $this->DBS['log_file'] = $log_file; $this->DBS['log_counter'] = $queries_count; } /** * Log query * * @param string $log_file */ public function log_query($log_file = 'sql_queries') { $q_time = ($this->cur_query_time >= 10) ? round($this->cur_query_time, 0) : sprintf('%.4f', $this->cur_query_time); $msg = []; $msg[] = round($this->sql_starttime); $msg[] = date('m-d H:i:s', $this->sql_starttime); $msg[] = sprintf('%-6s', $q_time); $msg[] = sprintf('%-4s', round(sys('la'), 1)); $msg[] = sprintf('%05d', getmypid()); $msg[] = $this->db_server; $msg[] = short_query($this->cur_query); $msg = implode(LOG_SEPR, $msg); $msg .= ($info = $this->query_info()) ? ' # ' . $info : ''; $msg .= ' # ' . $this->debug_find_source() . ' '; $msg .= defined('IN_CRON') ? 'cron' : basename($_SERVER['REQUEST_URI']); bb_log($msg . LOG_LF, $log_file); } /** * Log slow query * * @param string $log_file */ public function log_slow_query($log_file = 'sql_slow_bb') { if (!defined('IN_FIRST_SLOW_QUERY') && CACHE('bb_cache')->get('dont_log_slow_query')) { return; } $this->log_query($log_file); } /** * Log error */ public function log_error() { if (!SQL_LOG_ERRORS) { return; } $msg = []; $err = $this->sql_error(); $msg[] = str_compact(sprintf('#%06d %s', $err['code'], $err['message'])); $msg[] = ''; $msg[] = str_compact($this->cur_query); $msg[] = ''; $msg[] = 'Source : ' . $this->debug_find_source() . " :: $this->db_server.$this->selected_db"; $msg[] = 'IP : ' . @$_SERVER['REMOTE_ADDR']; $msg[] = 'Date : ' . date('Y-m-d H:i:s'); $msg[] = 'Agent : ' . @$_SERVER['HTTP_USER_AGENT']; $msg[] = 'Req_URI : ' . @$_SERVER['REQUEST_URI']; $msg[] = 'Referer : ' . @$_SERVER['HTTP_REFERER']; $msg[] = 'Method : ' . @$_SERVER['REQUEST_METHOD']; $msg[] = 'PID : ' . sprintf('%05d', getmypid()); $msg[] = 'Request : ' . trim(print_r($_REQUEST, true)) . str_repeat('_', 78) . LOG_LF; $msg[] = ''; bb_log($msg, 'sql_error_bb'); } /** * Explain queries * * @param $mode * @param string $html_table * @param string $row * * @return bool|string */ public function explain($mode, $html_table = '', $row = '') { $query = str_compact($this->cur_query); // remove comments $query = preg_replace('#(\s*)(/\*)(.*)(\*/)(\s*)#', '', $query); switch ($mode) { case 'start': $this->explain_hold = ''; if (preg_match('#UPDATE ([a-z0-9_]+).*?WHERE(.*)/#', $query, $m)) { $query = "SELECT * FROM $m[1] WHERE $m[2]"; } elseif (preg_match('#DELETE FROM ([a-z0-9_]+).*?WHERE(.*)#s', $query, $m)) { $query = "SELECT * FROM $m[1] WHERE $m[2]"; } if (preg_match('#^SELECT#', $query)) { $html_table = false; if ($result = mysqli_query($this->link, "EXPLAIN $query")) { while ($row = mysqli_fetch_assoc($result)) { $html_table = $this->explain('add_explain_row', $html_table, $row); } } if ($html_table) { $this->explain_hold .= ''; } } break; case 'stop': if (!$this->explain_hold) { break; } $id = $this->dbg_id - 1; $htid = 'expl-' . spl_object_hash($this->link) . '-' . $id; $dbg = $this->dbg[$id]; $this->explain_out .= '
' . $dbg['src'] . ' [' . sprintf('%.4f', $dbg['time']) . ' s] ' . $dbg['info'] . ' | ' . "$this->db_server.$this->selected_db" . ' :: Query #' . ($this->num_queries + 1) . ' |
---|---|
' . $this->explain_hold . ' |