*/ private static $cache = array(); /** * Return array if primary key is composite key * * @param mysqli|resource $dbh database connection * @param string $tableName table name * @param null|callable $logCallback log callback * * @return false|string|string[] return unique index column ky or false if don't exists */ public static function getUniqueIndexColumn($dbh, $tableName, $logCallback = null) { $cacheKey = self::CACHE_PREFIX_PRIMARY_KEY_COLUMN . $tableName; if (!isset(self::$cache[$cacheKey])) { $query = 'SHOW COLUMNS FROM `' . self::realEscapeString($dbh, $tableName) . '` WHERE `Key` IN ("PRI","UNI")'; if (($result = self::query($dbh, $query)) === false) { if (is_callable($logCallback)) { call_user_func($logCallback, $dbh, $result, $query); } throw new \Exception('SHOW KEYS QUERY ERROR: ' . self::error($dbh)); } if (is_callable($logCallback)) { call_user_func($logCallback, $dbh, $result, $query); } if (self::numRows($result) == 0) { self::$cache[$cacheKey] = false; } else { $primary = false; $excludePrimary = false; $unique = false; while ($row = self::fetchAssoc($result)) { switch ($row['Key']) { case 'PRI': if ($primary === false) { $primary = $row['Field']; } else { if (is_scalar($primary)) { $primary = array($primary); } $primary[] = $row['Field']; } if (preg_match('/^(?:var)?binary/i', $row['Type'])) { // exclude binary or varbynary columns $excludePrimary = true; } break; case 'UNI': if (!preg_match('/^(?:var)?binary/i', $row['Type'])) { // exclude binary or varbynary columns $unique = $row['Field']; } break; default: break; } } if ($primary !== false && $excludePrimary === false) { self::$cache[$cacheKey] = $primary; } elseif ($unique !== false) { self::$cache[$cacheKey] = $unique; } else { self::$cache[$cacheKey] = false; } } self::freeResult($result); } return self::$cache[$cacheKey]; } /** * Escape the regex for mysql queries, the mysqli_real_escape must be applied anyway to the generated string * * @param string $regex Regex * * @return string Escaped regex */ public static function quoteRegex($regex) { // preg_quote takes a string and escapes special characters with a backslash. // It is meant for PHP regexes, not MySQL regexes, and it does not escape &, // which is needed for MySQL. So we only need to modify it like so: // https://stackoverflow.com/questions/3782379/whats-the-best-way-to-escape-user-input-for-regular-expressions-in-mysql return preg_replace('/&/', '\\&', preg_quote($regex, null /* no delimiter */)); } /** * Returns the offset from the current row * * @param mixed[] $row current database row * @param int|string|string[] $indexColumns columns of the row that generated the index offset * @param mixed $lastOffset last offset * * @return mixed */ public static function getOffsetFromRowAssoc($row, $indexColumns, $lastOffset) { if (is_array($indexColumns)) { $result = array(); foreach ($indexColumns as $col) { $result[$col] = isset($row[$col]) ? $row[$col] : 0; } return $result; } elseif (strlen($indexColumns) > 0) { return isset($row[$indexColumns]) ? $row[$indexColumns] : 0; } else { if (is_scalar($lastOffset)) { return $lastOffset + 1; } else { return $lastOffset; } } } /** * This function performs a select by structuring the primary key as offset if the table has a primary key. * For optimization issues, no checks are performed on the input query and it is assumed that the select has at least a where value. * If there are no conditions, you still have to perform an always true condition, for example * SELECT * FROM `copy1_postmeta` WHERE 1 * * @param mysqli|resource $dbh database connection * @param string $query query string * @param string $table table name * @param int $offset row offset * @param int $limit limit of query, 0 no limit * @param mixed $lastRowOffset last offset to use on next function call * @param null|callable $logCallback log callback * * @return mysqli_result */ public static function selectUsingPrimaryKeyAsOffset($dbh, $query, $table, $offset, $limit, &$lastRowOffset = null, $logCallback = null) { $where = ''; $orderby = ''; $offsetStr = ''; $limitStr = $limit > 0 ? ' LIMIT ' . $limit : ''; if (($primaryColumn = self::getUniqueIndexColumn($dbh, $table, $logCallback)) == false) { $offsetStr = ' OFFSET ' . (is_scalar($offset) ? $offset : 0); } else { if (is_array($primaryColumn)) { // COMPOSITE KEY $orderByCols = array(); foreach ($primaryColumn as $colIndex => $col) { $orderByCols[] = '`' . $col . '` ASC'; } $orderby = ' ORDER BY ' . implode(',', $orderByCols); } else { $orderby = ' ORDER BY `' . $primaryColumn . '` ASC'; } $where = self::getOffsetKeyCondition($dbh, $primaryColumn, $offset); } $query .= $where . $orderby . $limitStr . $offsetStr; if (($result = self::query($dbh, $query)) === false) { if (is_callable($logCallback)) { call_user_func($logCallback, $dbh, $result, $query); } throw new \Exception('SELECT ERROR: ' . self::error($dbh) . ' QUERY: ' . $query); } if (is_callable($logCallback)) { call_user_func($logCallback, $dbh, $result, $query); } if (self::dbConnTypeByResult($result) === self::CONN_MYSQLI) { if ($primaryColumn == false) { $lastRowOffset = $offset + $result->num_rows; } else { if ($result->num_rows == 0) { $lastRowOffset = $offset; } else { $result->data_seek(($result->num_rows - 1)); $row = $result->fetch_assoc(); if (is_array($primaryColumn)) { $lastRowOffset = array(); foreach ($primaryColumn as $col) { $lastRowOffset[$col] = $row[$col]; } } else { $lastRowOffset = $row[$primaryColumn]; } $result->data_seek(0); } } } else { if ($primaryColumn == false) { $lastRowOffset = $offset + mysql_num_rows($result); // @phpstan-ignore-line } else { if (mysql_num_rows($result) == 0) { // @phpstan-ignore-line $lastRowOffset = $offset; } else { mysql_data_seek($result, (mysql_num_rows($result) - 1)); // @phpstan-ignore-line $row = mysql_fetch_assoc($result); // @phpstan-ignore-line if (is_array($primaryColumn)) { $lastRowOffset = array(); foreach ($primaryColumn as $col) { $lastRowOffset[$col] = $row[$col]; } } else { $lastRowOffset = $row[$primaryColumn]; } mysql_data_seek($result, 0); // @phpstan-ignore-line } } } return $result; } /** * Depending on the structure type of the primary key returns the condition to position at the right offset * * @param mysqli|resource $dbh database connection * @param string|string[] $primaryColumn primaricolumng index * @param mixed $offset offset * * @return string */ protected static function getOffsetKeyCondition($dbh, $primaryColumn, $offset) { $condition = ''; if ($offset === 0) { return ''; } // COUPOUND KEY if (is_array($primaryColumn)) { $isFirstCond = true; foreach ($primaryColumn as $colIndex => $col) { if (is_array($offset) && isset($offset[$col])) { if ($isFirstCond) { $isFirstCond = false; } else { $condition .= ' OR '; } $condition .= ' ('; for ($prevColIndex = 0; $prevColIndex < $colIndex; $prevColIndex++) { $condition .= ' `' . $primaryColumn[$prevColIndex] . '` = "' . self::realEscapeString($dbh, $offset[$primaryColumn[$prevColIndex]]) . '" AND '; } $condition .= ' `' . $col . '` > "' . self::realEscapeString($dbh, $offset[$col]) . '")'; } } } else { $condition = '`' . $primaryColumn . '` > "' . self::realEscapeString($dbh, (is_scalar($offset) ? $offset : 0)) . '"'; } return (strlen($condition) ? ' AND (' . $condition . ')' : ''); } /** * get current database engine (mysql, maria, percona) * * @param mysqli|resource $dbh database connection * * @return string */ public static function getDBEngine($dbh) { if (($result = self::query($dbh, "SHOW VARIABLES LIKE 'version%'")) === false) { // on query error assume is mysql. return self::DB_ENGINE_MYSQL; } $rows = array(); while ($row = self::fetchRow($result)) { $rows[] = $row; } self::freeResult($result); $version = isset($rows[0][1]) ? $rows[0][1] : false; $versionComment = isset($rows[1][1]) ? $rows[1][1] : false; //Default is mysql if ($version === false && $versionComment === false) { return self::DB_ENGINE_MYSQL; } if (stripos($version, 'maria') !== false || stripos($versionComment, 'maria') !== false) { return self::DB_ENGINE_MARIA; } if (stripos($version, 'percona') !== false || stripos($versionComment, 'percona') !== false) { return self::DB_ENGINE_PERCONA; } return self::DB_ENGINE_MYSQL; } /** * Escape string * * @param resource|mysqli $dbh database connection * @param string $string string to escape * * @return string Returns an escaped string. */ public static function realEscapeString($dbh, $string) { if (self::dbConnType($dbh) === self::CONN_MYSQLI) { return mysqli_real_escape_string($dbh, $string); } else { return mysql_real_escape_string($string, $dbh); // @phpstan-ignore-line } } /** * * @param resource|mysqli $dbh database connection * @param string $query query string * * @return mixed
Returns FALSE
on failure. For successful SELECT, SHOW, DESCRIBE or
* EXPLAIN queries mysqli_query() will return a mysqli_result object.
* For other successful queries mysqli_query() will return TRUE
.