From cc271ee1545af4c59329402e145a0ea2b9e991f9 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Mon, 29 Jun 2020 15:45:01 +0900 Subject: [PATCH] Initial implementation of DB class based on PDO MySQL driver --- common/framework/db.php | 539 +++++++++++++++++++++ common/framework/exceptions/dberror.php | 18 + common/framework/exceptions/queryerror.php | 2 +- common/legacy.php | 2 +- 4 files changed, 559 insertions(+), 2 deletions(-) create mode 100644 common/framework/db.php create mode 100644 common/framework/exceptions/dberror.php diff --git a/common/framework/db.php b/common/framework/db.php new file mode 100644 index 000000000..1743dafb2 --- /dev/null +++ b/common/framework/db.php @@ -0,0 +1,539 @@ +_prefix = $config['prefix'] ?: $this->_prefix; + $this->_charset = $config['charset'] ?: $this->_charset; + $this->_engine = $config['engine'] ?: $this->_engine; + + // Connect to the DB. + $dsn = 'mysql:host=' . $config['host']; + $dsn .= (isset($config['port']) && $config['port'] != 3306) ? (';port=' . $config['port']) : ''; + $dsn .= ';dbname=' . $config['database']; + $dsn .= ';charset=' . $this->_charset; + $options = array( + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + \PDO::ATTR_EMULATE_PREPARES => false, + \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false, + ); + try + { + $this->_handle = new \PDO($dsn, $config['user'], $config['pass'], $options); + } + catch (\PDOException $e) + { + throw new Exceptions\DBError($e->getMessage(), $e->getCode()); + } + + // Get the DB version. + $this->db_version = $this->_handle->getAttribute(\PDO::ATTR_SERVER_VERSION); + } + + /** + * Get the raw PDO handle. + * + * @return PDO + */ + public function getHandle(): \PDO + { + return $this->_handle; + } + + /** + * Execute an XML-defined query. + * + * @param string $query_id + * @param array $args + * @param array $columns + * @param string $type + * @return \BaseObject + */ + public function executeQuery(string $query_id, $args = [], $column_list = []): \BaseObject + { + // Validate the args. + if (is_object($args)) + { + $args = get_object_vars($args); + } + if (!is_array($args)) + { + return $this->setError(-1, 'Invalid query arguments.'); + } + + // Force the column list to a numerical array. + $column_list = is_array($column_list) ? array_values($column_list) : array(); + + // Start measuring elapsed time. + $class_start_time = microtime(true); + + // Get the name of the XML file. + $parts = explode('.', $query_id); + if (count($parts) === 2) + { + array_unshift($parts, 'modules'); + } + $filename = \RX_BASEDIR . $parts[0] . '/' . $parts[1] . '/queries/' . $parts[2] . '.xml'; + if (!Storage::exists($filename)) + { + return $this->setError(-1, 'Query \'' . $query_id . '\' does not exist.'); + } + + // Parse and cache the XML file. + $cache_key = sprintf('query:%s:%d', $filename, filemtime($filename)); + $query = Cache::get($cache_key); + if (!$query) + { + $query = Parsers\DBQueryParser::loadXML($filename); + if ($query) + { + Cache::set($cache_key, $query, 0, true); + } + else + { + return $this->setError(-1, 'Query \'' . $query_id . '\' cannot be parsed.'); + } + } + + // Get the query string and parameters. + try + { + $query_string = $query->getQueryString($this->_prefix, $args, $column_list); + $query_params = $query->getQueryParams(); + } + catch (Exceptions\QueryError $e) + { + return $this->setError(-1, $e->getMessage()); + } + + // Prepare and execute the query. + try + { + $query_start_time = microtime(true); + $this->_last_stmt = $this->_query($query_string, $query_params); + $query_elapsed_time = microtime(true) - $query_start_time; + $result = $this->_fetch($this->_last_stmt); + } + catch (\PDOException $e) + { + return $this->setError(-1, $e->getMessage()); + } + + // Compose the result object. + $output = new \BaseObject; + $output->add('_query', $query_string); + $output->add('_elapsed_time', sprintf("%0.5f", $query_elapsed_time)); + $output->data = $result; + + // Compose statistics about elapsed time. + $class_elapsed_time = microtime(true) - $class_start_time; + $this->_query_time += $query_elapsed_time; + $this->_total_time += $class_elapsed_time; + + // Return the complete result. + return $output; + } + + /** + * Execute a literal query string. + * + * This method should not be public, as it starts with an underscore. + * But since there are many legacy apps that rely on it, we will leave it public. + * + * @param string $query_string + * @param array $query_params + * @return \PDOStatement + */ + public function _query(string $query_string, array $query_params = []): \PDOStatement + { + if (count($query_params)) + { + $stmt = $this->_handle->prepare($query_string); + $stmt->execute($query_params); + } + else + { + $stmt = $this->_handle->query($query_string); + } + + return $stmt; + } + + /** + * Fetch results from a query. + * + * This method should not be public, as it starts with an underscore. + * But since there are many legacy apps that rely on it, we will leave it public. + * + * @param \PDOStatement $stmt + * @param int $last_index + * @return array|object + */ + public function _fetch(\PDOStatement $stmt, int $last_index = 0) + { + $result = array(); + $index = $last_index; + $step = $last_index !== 0 ? -1 : 1; + + while ($row = $stmt->fetchObject()) + { + $result[$index] = $row; + $index += $step; + } + + $stmt->closeCursor(); + + if ($last_index === 0 && count($result) === 1) + { + return $result[0]; + } + else + { + return $result; + } + } + + /** + * Begin a transaction. + * + * @return int + */ + public function begin(): int + { + if (!$this->_handle->inTransaction()) + { + $this->_handle->beginTransaction(); + } + $this->_transaction_level++; + return $this->_transaction_level; + } + + /** + * Roll back a transaction. + * + * @return int + */ + public function rollback(): int + { + if ($this->_handle->inTransaction() && $this->_transaction_level === 1) + { + $this->_handle->rollBack(); + } + $this->_transaction_level--; + return $this->_transaction_level; + } + + /** + * Commit a transaction. + * + * @return int + */ + public function commit(): int + { + if ($this->_handle->inTransaction() && $this->_transaction_level === 1) + { + $this->_handle->commit(); + } + $this->_transaction_level--; + return $this->_transaction_level; + } + + /** + * Get the number of rows affected by the last statement. + * + * @return int + */ + public function getAffectedRows(): int + { + return $this->_last_stmt ? intval($this->_last_stmt->rowCount()) : 0; + } + + /** + * Get the auto-incremented ID generated by the last statement. + * + * @return int + */ + public function getInsertID(): int + { + return intval($this->_handle->lastInsertId()); + } + + /** + * Get the next global sequence value. + */ + public function getNextSequence() + { + $this->_query(sprintf('INSERT INTO `%ssequence` (seq) VALUES (0)', $this->_prefix)); + $sequence = $this->getInsertID(); + if($sequence % 10000 == 0) + { + $this->_query(sprintf('DELETE FROM `%ssequence` WHERE seq < %d', $this->_prefix, $sequence)); + } + return $sequence; + } + + /** + * Drop a table. + * + * @param string $table_name + * @return BaseObject + */ + public function dropTable(string $table_name): \BaseObject + { + return new \BaseObject; + } + + /** + * Check if the last statement produced an error. + * + * @return bool + */ + public function isError(): bool + { + return $this->_errno !== 0 ? true : false; + } + + /** + * Get the last error information. + * + * @return \BaseObject + */ + public function getError(): \BaseObject + { + return new \BaseObject($this->_errno, $this->_errstr); + } + + /** + * Set error information to instance properties. + * + * @param int $errno + * @param string $errstr + * @return BaseObject + */ + public function setError(int $errno = 0, string $errstr = 'success'): \BaseObject + { + $this->_errno = $errno; + $this->_errstr = $errstr; + return new \BaseObject($errno, $errstr); + } + + /** + * Send an entry to the query log for debugging. + * + * @param array $log + * @return void + */ + public function setQueryLog(array $log) + { + Debug::addQuery($log); + } + + /** + * ========================== DEPRECATED METHODS ========================== + * ==================== KEPT FOR COMPATIBILITY WITH XE ==================== + */ + + /** + * Old alias to getInstance(). + * + * @deprecated + * @return self + */ + public static function create(): self + { + return self::getInstance(); + } + + /** + * Get the list of supported database drivers. + * + * @deprecated + * @return array + */ + public static function getSupportedList(): array + { + return array( + (object)array( + 'db_type' => 'mysql', + 'enable' => extension_loaded('pdo_mysql'), + ), + ); + } + + /** + * Get the list of enabled database drivers. + * + * @deprecated + * @return array + */ + public static function getEnableList(): array + { + return array_filter(self::getSupportedList(), function($item) { + return $item->enable; + }); + } + + /** + * Get the list of disabled database drivers. + * + * @deprecated + * @return array + */ + public static function getDisableList(): array + { + return array_filter(self::getSupportedList(), function($item) { + return !$item->enable; + }); + } + + /** + * Check if the current instance is supported. + * + * @deprecated + * @return bool + */ + public function isSupported(): bool + { + return true; + } + + /** + * Check if the current instance is connected. + * + * @deprecated + * @return bool + */ + public function isConnected(): bool + { + return true; + } + + /** + * Close the DB connection. + * + * @deprecated + * @return bool + */ + public function close(): bool + { + return true; + } + + /** + * Methods related to the click count cache feature. + * + * @deprecated + * @return bool + */ + public function getCountCache(): bool + { + return false; + } + public function putCountCache(): bool + { + return false; + } + public function resetCountCache(): bool + { + return false; + } + + /** + * Other deprecated methods. + */ + public function _getSlaveConnectionStringIndex(): int + { + return 0; + } + public function _getConnection(): \PDO + { + return $this->getHandle(); + } + public function _dbInfoExists(): bool + { + return true; + } +} diff --git a/common/framework/exceptions/dberror.php b/common/framework/exceptions/dberror.php new file mode 100644 index 000000000..2d7048ef9 --- /dev/null +++ b/common/framework/exceptions/dberror.php @@ -0,0 +1,18 @@ +executeQuery($query_id, $args, $arg_columns); - if(isset($output->data) && !is_array($output->data) && countobj($output->data) > 0) + if(isset($output->data) && is_object($output->data)) { $output->data = array($output->data); }