From f642caf15dc0b9dd0ccf4b94bb3368a05f71bc27 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 1 Feb 2013 23:29:17 +0100 Subject: [PATCH] redis db backend WIP --- framework/db/redis/ActiveQuery.php | 172 +++++++++++ framework/db/redis/ActiveRecord.php | 145 +++++++++ framework/db/redis/ActiveRelation.php | 23 ++ framework/db/redis/Command.php | 567 ++++++++++++++++++++++++++++++++++ framework/db/redis/Connection.php | 248 +++++++++++++++ framework/db/redis/Transaction.php | 91 ++++++ framework/db/redis/schema.md | 35 +++ 7 files changed, 1281 insertions(+) create mode 100644 framework/db/redis/ActiveQuery.php create mode 100644 framework/db/redis/ActiveRecord.php create mode 100644 framework/db/redis/ActiveRelation.php create mode 100644 framework/db/redis/Command.php create mode 100644 framework/db/redis/Connection.php create mode 100644 framework/db/redis/Transaction.php create mode 100644 framework/db/redis/schema.md diff --git a/framework/db/redis/ActiveQuery.php b/framework/db/redis/ActiveQuery.php new file mode 100644 index 0000000..e24e0f3 --- /dev/null +++ b/framework/db/redis/ActiveQuery.php @@ -0,0 +1,172 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\redis; + +/** + * ActiveRecord is the base class for classes representing relational data in terms of objects. + * + * + * @author Carsten Brandt + * @since 2.0 + */ +class ActiveQuery extends \yii\db\ActiveQuery +{ + /** + * Executes query and returns all results as an array. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all() + { + $command = $this->createCommand(); + $rows = $command->queryAll(); + if ($rows !== array()) { + $models = $this->createModels($rows); + if (!empty($this->with)) { + $this->populateRelations($models, $this->with); + } + return $models; + } else { + return array(); + } + } + + /** + * Executes query and returns a single row of result. + * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], + * the query result may be either an array or an ActiveRecord object. Null will be returned + * if the query results in nothing. + */ + public function one() + { + $command = $this->createCommand(); + $row = $command->queryRow(); + if ($row !== false && !$this->asArray) { + /** @var $class ActiveRecord */ + $class = $this->modelClass; + $model = $class::create($row); + if (!empty($this->with)) { + $models = array($model); + $this->populateRelations($models, $this->with); + $model = $models[0]; + } + return $model; + } else { + return $row === false ? null : $row; + } + } + + /** + * Returns the number of records. + * @param string $q the COUNT expression. Defaults to '*'. + * Make sure you properly quote column names. + * @return integer number of records + */ + public function count($q = '*') + { + $this->select = array("COUNT($q)"); + return $this->createCommand()->queryScalar(); + } + + /** + * Returns the sum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names. + * @return integer the sum of the specified column values + */ + public function sum($q) + { + $this->select = array("SUM($q)"); + return $this->createCommand()->queryScalar(); + } + + /** + * Returns the average of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names. + * @return integer the average of the specified column values. + */ + public function average($q) + { + $this->select = array("AVG($q)"); + return $this->createCommand()->queryScalar(); + } + + /** + * Returns the minimum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names. + * @return integer the minimum of the specified column values. + */ + public function min($q) + { + $this->select = array("MIN($q)"); + return $this->createCommand()->queryScalar(); + } + + /** + * Returns the maximum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names. + * @return integer the maximum of the specified column values. + */ + public function max($q) + { + $this->select = array("MAX($q)"); + return $this->createCommand()->queryScalar(); + } + + /** + * Returns the query result as a scalar value. + * The value returned will be the first column in the first row of the query results. + * @return string|boolean the value of the first column in the first row of the query result. + * False is returned if the query result is empty. + */ + public function scalar() + { + return $this->createCommand()->queryScalar(); + } + + /** + * Returns a value indicating whether the query result contains any row of data. + * @return boolean whether the query result contains any row of data. + */ + public function exists() + { + $this->select = array(new Expression('1')); + return $this->scalar() !== false; + } + + /** + * Creates a DB command that can be used to execute this query. + * @param Connection $db the DB connection used to create the DB command. + * If null, the DB connection returned by [[modelClass]] will be used. + * @return Command the created DB command instance. + */ + public function createCommand($db = null) + { + /** @var $modelClass ActiveRecord */ + $modelClass = $this->modelClass; + if ($db === null) { + $db = $modelClass::getDb(); + } + if ($this->sql === null) { + if ($this->from === null) { + $tableName = $modelClass::tableName(); + $this->from = array($tableName); + } + /** @var $qb QueryBuilder */ + $qb = $db->getQueryBuilder(); + $this->sql = $qb->build($this); + } + return $db->createCommand($this->sql, $this->params); + } + +} diff --git a/framework/db/redis/ActiveRecord.php b/framework/db/redis/ActiveRecord.php new file mode 100644 index 0000000..001a2f7 --- /dev/null +++ b/framework/db/redis/ActiveRecord.php @@ -0,0 +1,145 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\redis; + +/** + * ActiveRecord is the base class for classes representing relational data in terms of objects. + * + * @include @yii/db/ActiveRecord.md + * + * @author Carsten Brandt + * @since 2.0 + */ +abstract class ActiveRecord extends \yii\db\ActiveRecord +{ + /** + * Returns the list of all attribute names of the model. + * The default implementation will return all column names of the table associated with this AR class. + * @return array list of attribute names. + */ + public function attributes() + { + return array(); + } + + /** + * Returns the database connection used by this AR class. + * By default, the "db" application component is used as the database connection. + * You may override this method if you want to use a different database connection. + * @return Connection the database connection used by this AR class. + */ + public static function getDb() + { + // TODO + return \Yii::$application->getDb(); + } + + /** + * Creates an [[ActiveQuery]] instance for query purpose. + * + * @include @yii/db/ActiveRecord-find.md + * + * @param mixed $q the query parameter. This can be one of the followings: + * + * - a scalar value (integer or string): query by a single primary key value and return the + * corresponding record. + * - an array of name-value pairs: query by a set of column values and return a single record matching all of them. + * - null: return a new [[ActiveQuery]] object for further query purpose. + * + * @return ActiveQuery|ActiveRecord|null When `$q` is null, a new [[ActiveQuery]] instance + * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be + * returned (null will be returned if there is no matching). + * @see createQuery() + */ + public static function find($q = null) + { + // TODO + $query = static::createQuery(); + if (is_array($q)) { + return $query->where($q)->one(); + } elseif ($q !== null) { + // query by primary key + $primaryKey = static::primaryKey(); + return $query->where(array($primaryKey[0] => $q))->one(); + } + return $query; + } + + /** + * Creates an [[ActiveQuery]] instance with a given SQL statement. + * + * Note that because the SQL statement is already specified, calling additional + * query modification methods (such as `where()`, `order()`) on the created [[ActiveQuery]] + * instance will have no effect. However, calling `with()`, `asArray()` or `indexBy()` is + * still fine. + * + * Below is an example: + * + * ~~~ + * $customers = Customer::findBySql('SELECT * FROM tbl_customer')->all(); + * ~~~ + * + * @param string $sql the SQL statement to be executed + * @param array $params parameters to be bound to the SQL statement during execution. + * @return ActiveQuery the newly created [[ActiveQuery]] instance + */ + public static function findBySql($sql, $params = array()) + { + $query = static::createQuery(); + $query->sql = $sql; + return $query->params($params); + } + + + + /** + * Creates an [[ActiveQuery]] instance. + * This method is called by [[find()]], [[findBySql()]] and [[count()]] to start a SELECT query. + * You may override this method to return a customized query (e.g. `CustomerQuery` specified + * written for querying `Customer` purpose.) + * @return ActiveQuery the newly created [[ActiveQuery]] instance. + */ + public static function createQuery() + { + return new ActiveQuery(array( + 'modelClass' => get_called_class(), + )); + } + + + /** + * Returns the schema information of the DB table associated with this AR class. + * @return TableSchema the schema information of the DB table associated with this AR class. + */ + public static function getTableSchema() + { + return static::getDb()->getTableSchema(static::tableName()); + } + + /** + * Returns the primary key name(s) for this AR class. + * The default implementation will return the primary key(s) as declared + * in the DB table that is associated with this AR class. + * + * If the DB table does not declare any primary key, you should override + * this method to return the attributes that you want to use as primary keys + * for this AR class. + * + * Note that an array should be returned even for a table with single primary key. + * + * @return string[] the primary keys of the associated database table. + */ + public static function primaryKey() + { + return static::getTableSchema()->primaryKey; + } + +} diff --git a/framework/db/redis/ActiveRelation.php b/framework/db/redis/ActiveRelation.php new file mode 100644 index 0000000..a205cd0 --- /dev/null +++ b/framework/db/redis/ActiveRelation.php @@ -0,0 +1,23 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\redis; + +/** + * ActiveRecord is the base class for classes representing relational data in terms of objects. + * + * + * @author Carsten Brandt + * @since 2.0 + */ +class ActiveRelation extends \yii\db\ActiveRelation +{ + +} diff --git a/framework/db/redis/Command.php b/framework/db/redis/Command.php new file mode 100644 index 0000000..714f012 --- /dev/null +++ b/framework/db/redis/Command.php @@ -0,0 +1,567 @@ + + * @since 2.0 + */ +class Command extends \yii\base\Component +{ + /** + * @var Connection the DB connection that this command is associated with + */ + public $db; + /** + * @var array the parameter log information (name=>value) + */ + private $_params = array(); + + private $_query; + + + /** + * Determines the PDO type for the give PHP data value. + * @param mixed $data the data whose PDO type is to be determined + * @return integer the PDO type + * @see http://www.php.net/manual/en/pdo.constants.php + */ + private function getRedisType($data) + { + static $typeMap = array( + 'boolean' => \PDO::PARAM_BOOL, + 'integer' => \PDO::PARAM_INT, + 'string' => \PDO::PARAM_STR, + 'NULL' => \PDO::PARAM_NULL, + ); + $type = gettype($data); + return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; + } + + /** + * Executes the SQL statement. + * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs. + * No result set will be returned. + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] + * or [[bindValue()]] will be ignored. + * @return integer number of rows affected by the execution. + * @throws Exception execution failed + */ + public function execute() + { + $query = $this->_query; + + if ($this->_params === array()) { + $paramLog = ''; + } else { + $paramLog = "\nParameters: " . var_export($this->_params, true); + } + + \Yii::trace("Executing SQL: {$query}{$paramLog}", __CLASS__); + + if ($query == '') { + return 0; + } + + try { + if ($this->db->enableProfiling) { + \Yii::beginProfile(__METHOD__ . "($query)", __CLASS__); + } + + $n = $this->db->redis->send_command(array_merge(array($query), $this->_params)); + + if ($this->db->enableProfiling) { + \Yii::endProfile(__METHOD__ . "($query)", __CLASS__); + } + return $n; + } catch (\Exception $e) { + if ($this->db->enableProfiling) { + \Yii::endProfile(__METHOD__ . "($query)", __CLASS__); + } + $message = $e->getMessage(); + + \Yii::error("$message\nFailed to execute SQL: {$query}{$paramLog}", __CLASS__); + + throw new Exception($message, (int)$e->getCode()); + } + } + + /** + * Executes the SQL statement and returns query result. + * This method is for executing a SQL query that returns result set, such as `SELECT`. + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] + * or [[bindValue()]] will be ignored. + * @return DataReader the reader object for fetching the query result + * @throws Exception execution failed + */ + public function query($params = array()) + { + return $this->queryInternal('', $params); + } + + /** + * Executes the SQL statement and returns ALL rows at once. + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] + * or [[bindValue()]] will be ignored. + * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) + * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. + * @return array all rows of the query result. Each array element is an array representing a row of data. + * An empty array is returned if the query results in nothing. + * @throws Exception execution failed + */ + public function queryAll($params = array(), $fetchMode = null) + { + return $this->queryInternal('fetchAll', $params, $fetchMode); + } + + /** + * Executes the SQL statement and returns the first row of the result. + * This method is best used when only the first row of result is needed for a query. + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] + * or [[bindValue()]] will be ignored. + * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) + * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. + * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query + * results in nothing. + * @throws Exception execution failed + */ + public function queryRow($params = array(), $fetchMode = null) + { + return $this->queryInternal('fetch', $params, $fetchMode); + } + + /** + * Executes the SQL statement and returns the value of the first column in the first row of data. + * This method is best used when only a single value is needed for a query. + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] + * or [[bindValue()]] will be ignored. + * @return string|boolean the value of the first column in the first row of the query result. + * False is returned if there is no value. + * @throws Exception execution failed + */ + public function queryScalar($params = array()) + { + $result = $this->queryInternal('fetchColumn', $params, 0); + if (is_resource($result) && get_resource_type($result) === 'stream') { + return stream_get_contents($result); + } else { + return $result; + } + } + + /** + * Executes the SQL statement and returns the first column of the result. + * This method is best used when only the first column of result (i.e. the first element in each row) + * is needed for a query. + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] + * or [[bindValue()]] will be ignored. + * @return array the first column of the query result. Empty array is returned if the query results in nothing. + * @throws Exception execution failed + */ + public function queryColumn($params = array()) + { + return $this->queryInternal('fetchAll', $params, \PDO::FETCH_COLUMN); + } + + /** + * Performs the actual DB query of a SQL statement. + * @param string $method method of PDOStatement to be called + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] + * or [[bindValue()]] will be ignored. + * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) + * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. + * @return mixed the method execution result + * @throws Exception if the query causes any problem + */ + private function queryInternal($method, $params, $fetchMode = null) + { + $db = $this->db; + $sql = $this->getSql(); + $this->_params = array_merge($this->_params, $params); + if ($this->_params === array()) { + $paramLog = ''; + } else { + $paramLog = "\nParameters: " . var_export($this->_params, true); + } + + \Yii::trace("Querying SQL: {$sql}{$paramLog}", __CLASS__); + + /** @var $cache \yii\caching\Cache */ + if ($db->enableQueryCache && $method !== '') { + $cache = \Yii::$application->getComponent($db->queryCacheID); + } + + if (isset($cache)) { + $cacheKey = $cache->buildKey(__CLASS__, $db->dsn, $db->username, $sql, $paramLog); + if (($result = $cache->get($cacheKey)) !== false) { + \Yii::trace('Query result found in cache', __CLASS__); + return $result; + } + } + + try { + if ($db->enableProfiling) { + \Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); + } + + $this->prepare(); + if ($params === array()) { + $this->pdoStatement->execute(); + } else { + $this->pdoStatement->execute($params); + } + + if ($method === '') { + $result = new DataReader($this); + } else { + if ($fetchMode === null) { + $fetchMode = $this->fetchMode; + } + $result = call_user_func_array(array($this->pdoStatement, $method), (array)$fetchMode); + $this->pdoStatement->closeCursor(); + } + + if ($db->enableProfiling) { + \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + } + + if (isset($cache, $cacheKey)) { + $cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency); + \Yii::trace('Saved query result in cache', __CLASS__); + } + + return $result; + } catch (\Exception $e) { + if ($db->enableProfiling) { + \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + } + $message = $e->getMessage(); + \Yii::error("$message\nCommand::$method() failed: {$sql}{$paramLog}", __CLASS__); + $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; + throw new Exception($message, (int)$e->getCode(), $errorInfo); + } + } + + /** + * Creates an INSERT command. + * For example, + * + * ~~~ + * $connection->createCommand()->insert('tbl_user', array( + * 'name' => 'Sam', + * 'age' => 30, + * ))->execute(); + * ~~~ + * + * The method will properly escape the column names, and bind the values to be inserted. + * + * Note that the created command is not executed until [[execute()]] is called. + * + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column data (name=>value) to be inserted into the table. + * @return Command the command object itself + */ + public function insert($table, $columns) + { + $params = array(); + $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params); + return $this->setSql($sql)->bindValues($params); + } + + /** + * Creates a batch INSERT command. + * For example, + * + * ~~~ + * $connection->createCommand()->batchInsert('tbl_user', array('name', 'age'), array( + * array('Tom', 30), + * array('Jane', 20), + * array('Linda', 25), + * ))->execute(); + * ~~~ + * + * Not that the values in each row must match the corresponding column names. + * + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column names + * @param array $rows the rows to be batch inserted into the table + * @return Command the command object itself + */ + public function batchInsert($table, $columns, $rows) + { + $sql = $this->db->getQueryBuilder()->batchInsert($table, $columns, $rows); + return $this->setSql($sql); + } + + /** + * Creates an UPDATE command. + * For example, + * + * ~~~ + * $connection->createCommand()->update('tbl_user', array( + * 'status' => 1, + * ), 'age > 30')->execute(); + * ~~~ + * + * The method will properly escape the column names and bind the values to be updated. + * + * Note that the created command is not executed until [[execute()]] is called. + * + * @param string $table the table to be updated. + * @param array $columns the column data (name=>value) to be updated. + * @param mixed $condition the condition that will be put in the WHERE part. Please + * refer to [[Query::where()]] on how to specify condition. + * @param array $params the parameters to be bound to the command + * @return Command the command object itself + */ + public function update($table, $columns, $condition = '', $params = array()) + { + $sql = $this->db->getQueryBuilder()->update($table, $columns, $condition, $params); + return $this->setSql($sql)->bindValues($params); + } + + /** + * Creates a DELETE command. + * For example, + * + * ~~~ + * $connection->createCommand()->delete('tbl_user', 'status = 0')->execute(); + * ~~~ + * + * The method will properly escape the table and column names. + * + * Note that the created command is not executed until [[execute()]] is called. + * + * @param string $table the table where the data will be deleted from. + * @param mixed $condition the condition that will be put in the WHERE part. Please + * refer to [[Query::where()]] on how to specify condition. + * @param array $params the parameters to be bound to the command + * @return Command the command object itself + */ + public function delete($table, $condition = '', $params = array()) + { + $sql = $this->db->getQueryBuilder()->delete($table, $condition); + return $this->setSql($sql)->bindValues($params); + } + + + /** + * Creates a SQL command for creating a new DB table. + * + * The columns in the new table should be specified as name-definition pairs (e.g. 'name'=>'string'), + * where name stands for a column name which will be properly quoted by the method, and definition + * stands for the column type which can contain an abstract DB type. + * The method [[QueryBuilder::getColumnType()]] will be called + * to convert the abstract column types to physical ones. For example, `string` will be converted + * as `varchar(255)`, and `string not null` becomes `varchar(255) not null`. + * + * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly + * inserted into the generated SQL. + * + * @param string $table the name of the table to be created. The name will be properly quoted by the method. + * @param array $columns the columns (name=>definition) in the new table. + * @param string $options additional SQL fragment that will be appended to the generated SQL. + * @return Command the command object itself + */ + public function createTable($table, $columns, $options = null) + { + $sql = $this->db->getQueryBuilder()->createTable($table, $columns, $options); + return $this->setSql($sql); + } + + /** + * Creates a SQL command for renaming a DB table. + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * @return Command the command object itself + */ + public function renameTable($table, $newName) + { + $sql = $this->db->getQueryBuilder()->renameTable($table, $newName); + return $this->setSql($sql); + } + + /** + * Creates a SQL command for dropping a DB table. + * @param string $table the table to be dropped. The name will be properly quoted by the method. + * @return Command the command object itself + */ + public function dropTable($table) + { + $sql = $this->db->getQueryBuilder()->dropTable($table); + return $this->setSql($sql); + } + + /** + * Creates a SQL command for truncating a DB table. + * @param string $table the table to be truncated. The name will be properly quoted by the method. + * @return Command the command object itself + */ + public function truncateTable($table) + { + $sql = $this->db->getQueryBuilder()->truncateTable($table); + return $this->setSql($sql); + } + + /** + * Creates a SQL command for adding a new DB column. + * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method. + * @param string $column the name of the new column. The name will be properly quoted by the method. + * @param string $type the column type. [[\yii\db\QueryBuilder::getColumnType()]] will be called + * to convert the give column type to the physical one. For example, `string` will be converted + * as `varchar(255)`, and `string not null` becomes `varchar(255) not null`. + * @return Command the command object itself + */ + public function addColumn($table, $column, $type) + { + $sql = $this->db->getQueryBuilder()->addColumn($table, $column, $type); + return $this->setSql($sql); + } + + /** + * Creates a SQL command for dropping a DB column. + * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. + * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. + * @return Command the command object itself + */ + public function dropColumn($table, $column) + { + $sql = $this->db->getQueryBuilder()->dropColumn($table, $column); + return $this->setSql($sql); + } + + /** + * Creates a SQL command for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $oldName the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + * @return Command the command object itself + */ + public function renameColumn($table, $oldName, $newName) + { + $sql = $this->db->getQueryBuilder()->renameColumn($table, $oldName, $newName); + return $this->setSql($sql); + } + + /** + * Creates a SQL command for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the column type. [[\yii\db\QueryBuilder::getColumnType()]] will be called + * to convert the give column type to the physical one. For example, `string` will be converted + * as `varchar(255)`, and `string not null` becomes `varchar(255) not null`. + * @return Command the command object itself + */ + public function alterColumn($table, $column, $type) + { + $sql = $this->db->getQueryBuilder()->alterColumn($table, $column, $type); + return $this->setSql($sql); + } + + /** + * Creates a SQL command for adding a foreign key constraint to an existing table. + * The method will properly quote the table and column names. + * @param string $name the name of the foreign key constraint. + * @param string $table the table that the foreign key constraint will be added to. + * @param string $columns the name of the column to that the constraint will be added on. If there are multiple columns, separate them with commas. + * @param string $refTable the table that the foreign key references to. + * @param string $refColumns the name of the column that the foreign key references to. If there are multiple columns, separate them with commas. + * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @return Command the command object itself + */ + public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null) + { + $sql = $this->db->getQueryBuilder()->addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update); + return $this->setSql($sql); + } + + /** + * Creates a SQL command for dropping a foreign key constraint. + * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. + * @return Command the command object itself + */ + public function dropForeignKey($name, $table) + { + $sql = $this->db->getQueryBuilder()->dropForeignKey($name, $table); + return $this->setSql($sql); + } + + /** + * Creates a SQL command for creating a new index. + * @param string $name the name of the index. The name will be properly quoted by the method. + * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method. + * @param string $columns the column(s) that should be included in the index. If there are multiple columns, please separate them + * by commas. The column names will be properly quoted by the method. + * @param boolean $unique whether to add UNIQUE constraint on the created index. + * @return Command the command object itself + */ + public function createIndex($name, $table, $columns, $unique = false) + { + $sql = $this->db->getQueryBuilder()->createIndex($name, $table, $columns, $unique); + return $this->setSql($sql); + } + + /** + * Creates a SQL command for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + * @return Command the command object itself + */ + public function dropIndex($name, $table) + { + $sql = $this->db->getQueryBuilder()->dropIndex($name, $table); + return $this->setSql($sql); + } + + /** + * Creates a SQL command for resetting the sequence value of a table's primary key. + * The sequence will be reset such that the primary key of the next new row inserted + * will have the specified value or 1. + * @param string $table the name of the table whose primary key sequence will be reset + * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, + * the next new row's primary key will have a value 1. + * @return Command the command object itself + * @throws NotSupportedException if this is not supported by the underlying DBMS + */ + public function resetSequence($table, $value = null) + { + $sql = $this->db->getQueryBuilder()->resetSequence($table, $value); + return $this->setSql($sql); + } + + /** + * Builds a SQL command for enabling or disabling integrity check. + * @param boolean $check whether to turn on or off the integrity check. + * @param string $schema the schema name of the tables. Defaults to empty string, meaning the current + * or default schema. + * @return Command the command object itself + * @throws NotSupportedException if this is not supported by the underlying DBMS + */ + public function checkIntegrity($check = true, $schema = '') + { + $sql = $this->db->getQueryBuilder()->checkIntegrity($check, $schema); + return $this->setSql($sql); + } +} diff --git a/framework/db/redis/Connection.php b/framework/db/redis/Connection.php new file mode 100644 index 0000000..8623b22 --- /dev/null +++ b/framework/db/redis/Connection.php @@ -0,0 +1,248 @@ +close(); + return array_keys(get_object_vars($this)); + } + + /** + * Returns a value indicating whether the DB connection is established. + * @return boolean whether the DB connection is established + */ + public function getIsActive() + { + return $this->redis !== null; + } + + /** + * Establishes a DB connection. + * It does nothing if a DB connection has already been established. + * @throws Exception if connection fails + */ + public function open() + { + if ($this->redis === null) { + if (empty($this->dsn)) { + throw new InvalidConfigException('Connection.dsn cannot be empty.'); + } + // TODO parse DSN + $host = 'localhost'; + $port = 6379; + try { + \Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); + // TODO connection to redis seems to be very easy, consider writing own connect + $this->redis = new \Jamm\Memory\RedisServer($host, $port); + $this->initConnection(); + } + catch (\PDOException $e) { + \Yii::error("Failed to open DB connection ({$this->dsn}): " . $e->getMessage(), __CLASS__); + $message = YII_DEBUG ? 'Failed to open DB connection: ' . $e->getMessage() : 'Failed to open DB connection.'; + throw new Exception($message, (int)$e->getCode(), $e->errorInfo); + } + } + } + + /** + * Closes the currently active DB connection. + * It does nothing if the connection is already closed. + */ + public function close() + { + if ($this->redis !== null) { + \Yii::trace('Closing DB connection: ' . $this->dsn, __CLASS__); + $this->redis = null; + $this->_transaction = null; + } + } + + /** + * Initializes the DB connection. + * This method is invoked right after the DB connection is established. + * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES` + * if [[emulatePrepare]] is true, and sets the database [[charset]] if it is not empty. + * It then triggers an [[EVENT_AFTER_OPEN]] event. + */ + protected function initConnection() + { + $this->trigger(self::EVENT_AFTER_OPEN); + } + + + /** + * Creates a command for execution. + * @param string $query the SQL statement to be executed + * @param array $params the parameters to be bound to the SQL statement + * @return Command the DB command + */ + public function createCommand($query = null, $params = array()) + { + $this->open(); + $command = new Command(array( + 'db' => $this, + 'query' => $query, + )); + return $command->addValues($params); + } + + /** + * Returns the currently active transaction. + * @return Transaction the currently active transaction. Null if no active transaction. + */ + public function getTransaction() + { + return $this->_transaction && $this->_transaction->isActive ? $this->_transaction : null; + } + + /** + * Starts a transaction. + * @return Transaction the transaction initiated + */ + public function beginTransaction() + { + $this->open(); + $this->_transaction = new Transaction(array( + 'db' => $this, + )); + $this->_transaction->begin(); + return $this->_transaction; + } + + /** + * Returns the schema information for the database opened by this connection. + * @return Schema the schema information for the database opened by this connection. + * @throws NotSupportedException if there is no support for the current driver type + */ + public function getSchema() + { + $driver = $this->getDriverName(); + throw new NotSupportedException("Connection does not support reading schema information for '$driver' DBMS."); + } + + /** + * Returns the query builder for the current DB connection. + * @return QueryBuilder the query builder for the current DB connection. + */ + public function getQueryBuilder() + { + return $this->getSchema()->getQueryBuilder(); + } + + /** + * Obtains the schema information for the named table. + * @param string $name table name. + * @param boolean $refresh whether to reload the table schema even if it is found in the cache. + * @return TableSchema table schema information. Null if the named table does not exist. + */ + public function getTableSchema($name, $refresh = false) + { + return $this->getSchema()->getTableSchema($name, $refresh); + } + + /** + * Returns the name of the DB driver for the current [[dsn]]. + * @return string name of the DB driver + */ + public function getDriverName() + { + if (($pos = strpos($this->dsn, ':')) !== false) { + return strtolower(substr($this->dsn, 0, $pos)); + } else { + return 'redis'; + } + } + + /** + * Returns the statistical results of SQL queries. + * The results returned include the number of SQL statements executed and + * the total time spent. + * In order to use this method, [[enableProfiling]] has to be set true. + * @return array the first element indicates the number of SQL statements executed, + * and the second element the total time spent in SQL execution. + * @see \yii\logging\Logger::getProfiling() + */ + public function getQuerySummary() + { + $logger = \Yii::getLogger(); + $timings = $logger->getProfiling(array('yii\db\Command::query', 'yii\db\Command::execute')); + $count = count($timings); + $time = 0; + foreach ($timings as $timing) { + $time += $timing[1]; + } + return array($count, $time); + } +} diff --git a/framework/db/redis/Transaction.php b/framework/db/redis/Transaction.php new file mode 100644 index 0000000..721a7be --- /dev/null +++ b/framework/db/redis/Transaction.php @@ -0,0 +1,91 @@ +_active; + } + + /** + * Begins a transaction. + * @throws InvalidConfigException if [[connection]] is null + */ + public function begin() + { + if (!$this->_active) { + if ($this->db === null) { + throw new InvalidConfigException('Transaction::db must be set.'); + } + \Yii::trace('Starting transaction', __CLASS__); + $this->db->open(); + $this->db->createCommand('MULTI')->execute(); + $this->_active = true; + } + } + + /** + * Commits a transaction. + * @throws Exception if the transaction or the DB connection is not active. + */ + public function commit() + { + if ($this->_active && $this->db && $this->db->isActive) { + \Yii::trace('Committing transaction', __CLASS__); + $this->db->createCommand('EXEC')->execute(); + // TODO handle result of EXEC + $this->_active = false; + } else { + throw new Exception('Failed to commit transaction: transaction was inactive.'); + } + } + + /** + * Rolls back a transaction. + * @throws Exception if the transaction or the DB connection is not active. + */ + public function rollback() + { + if ($this->_active && $this->db && $this->db->isActive) { + \Yii::trace('Rolling back transaction', __CLASS__); + $this->db->pdo->commit(); + $this->_active = false; + } else { + throw new Exception('Failed to roll back transaction: transaction was inactive.'); + } + } +} diff --git a/framework/db/redis/schema.md b/framework/db/redis/schema.md new file mode 100644 index 0000000..1bd45b3 --- /dev/null +++ b/framework/db/redis/schema.md @@ -0,0 +1,35 @@ +To allow AR to be stored in redis we need a special Schema for it. + +HSET prefix:className:primaryKey + + +http://redis.io/commands + +Current Redis connection: +https://github.com/jamm/Memory + + +# Queries + +wrap all these in transactions MULTI + +## insert + +SET all attribute key-value pairs +SET all relation key-value pairs +make sure to create back-relations + +## update + +SET all attribute key-value pairs +SET all relation key-value pairs + + +## delete + +DEL all attribute key-value pairs +DEL all relation key-value pairs +make sure to update back-relations + + +http://redis.io/commands/hmget sounds suiteable! \ No newline at end of file