diff --git a/extensions/sphinx/IndexSchema.php b/extensions/sphinx/IndexSchema.php new file mode 100644 index 0000000..81ba075 --- /dev/null +++ b/extensions/sphinx/IndexSchema.php @@ -0,0 +1,82 @@ + + * @since 2.0 + */ +class IndexSchema extends Object +{ + /** + * @var string name of the schema that this index belongs to. + */ + public $schemaName; + /** + * @var string name of this index. + */ + public $name; + /** + * @var string[] primary keys of this index. + */ + public $primaryKey = []; + /** + * @var ColumnSchema[] column metadata of this index. Each array element is a [[ColumnSchema]] object, indexed by column names. + */ + public $columns = []; + + /** + * Gets the named column metadata. + * This is a convenient method for retrieving a named column even if it does not exist. + * @param string $name column name + * @return ColumnSchema metadata of the named column. Null if the named column does not exist. + */ + public function getColumn($name) + { + return isset($this->columns[$name]) ? $this->columns[$name] : null; + } + + /** + * Returns the names of all columns in this table. + * @return array list of column names + */ + public function getColumnNames() + { + return array_keys($this->columns); + } + + /** + * Manually specifies the primary key for this table. + * @param string|array $keys the primary key (can be composite) + * @throws InvalidParamException if the specified key cannot be found in the table. + */ + public function fixPrimaryKey($keys) + { + if (!is_array($keys)) { + $keys = [$keys]; + } + $this->primaryKey = $keys; + foreach ($this->columns as $column) { + $column->isPrimaryKey = false; + } + foreach ($keys as $key) { + if (isset($this->columns[$key])) { + $this->columns[$key]->isPrimaryKey = true; + } else { + throw new InvalidParamException("Primary key '$key' cannot be found in index '{$this->name}'."); + } + } + } +} \ No newline at end of file diff --git a/extensions/sphinx/Schema.php b/extensions/sphinx/Schema.php index b9a34be..8313ae4 100644 --- a/extensions/sphinx/Schema.php +++ b/extensions/sphinx/Schema.php @@ -7,17 +7,48 @@ namespace yii\sphinx; -use yii\db\TableSchema; +use yii\base\Object; +use yii\caching\Cache; +use Yii; +use yii\caching\GroupDependency; /** - * Class Schema + * Schema represents the Sphinx schema information. * * @author Paul Klimov * @since 2.0 */ -class Schema extends \yii\db\mysql\Schema +class Schema extends Object { /** + * The followings are the supported abstract column data types. + */ + const TYPE_PK = 'pk'; + const TYPE_STRING = 'string'; + const TYPE_INTEGER = 'integer'; + const TYPE_BIGINT = 'bigint'; + const TYPE_FLOAT = 'float'; + const TYPE_TIMESTAMP = 'timestamp'; + const TYPE_BOOLEAN = 'boolean'; + + /** + * @var Connection the Sphinx connection + */ + public $db; + /** + * @var array list of ALL index names in the Sphinx + */ + private $_indexNames = []; + /** + * @var array list of loaded index metadata (index name => IndexSchema) + */ + private $_indexes = []; + /** + * @var QueryBuilder the query builder for this Sphinx connection + */ + private $_builder; + + /** * @var array mapping from physical column types (keys) to abstract column types (values) */ public $typeMap = [ @@ -35,8 +66,200 @@ class Schema extends \yii\db\mysql\Schema ]; /** - * Creates a query builder for the database. - * This method may be overridden by child classes to create a DBMS-specific query builder. + * Loads the metadata for the specified index. + * @param string $name index name + * @return IndexSchema driver dependent index metadata. Null if the index does not exist. + */ + protected function loadTableSchema($name) + { + $index = new IndexSchema; + $this->resolveIndexNames($index, $name); + + if ($this->findColumns($index)) { + return $index; + } else { + return null; + } + } + + /** + * Resolves the index name and schema name (if any). + * @param IndexSchema $index the index metadata object + * @param string $name the index name + */ + protected function resolveIndexNames($index, $name) + { + $parts = explode('.', str_replace('`', '', $name)); + if (isset($parts[1])) { + $index->schemaName = $parts[0]; + $index->name = $parts[1]; + } else { + $index->name = $parts[0]; + } + } + + /** + * Obtains the metadata for the named index. + * @param string $name index name. The index name may contain schema name if any. Do not quote the index name. + * @param boolean $refresh whether to reload the index schema even if it is found in the cache. + * @return IndexSchema index metadata. Null if the named index does not exist. + */ + public function getIndexSchema($name, $refresh = false) + { + if (isset($this->_indexes[$name]) && !$refresh) { + return $this->_indexes[$name]; + } + + $db = $this->db; + $realName = $this->getRawIndexName($name); + + if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) { + /** @var $cache Cache */ + $cache = is_string($db->schemaCache) ? Yii::$app->getComponent($db->schemaCache) : $db->schemaCache; + if ($cache instanceof Cache) { + $key = $this->getCacheKey($name); + if ($refresh || ($index = $cache->get($key)) === false) { + $index = $this->loadTableSchema($realName); + if ($index !== null) { + $cache->set($key, $index, $db->schemaCacheDuration, new GroupDependency($this->getCacheGroup())); + } + } + return $this->_indexes[$name] = $index; + } + } + return $this->_indexes[$name] = $index = $this->loadTableSchema($realName); + } + + /** + * Returns the cache key for the specified index name. + * @param string $name the index name + * @return mixed the cache key + */ + protected function getCacheKey($name) + { + return [ + __CLASS__, + $this->db->dsn, + $this->db->username, + $name, + ]; + } + + /** + * Returns the cache group name. + * This allows [[refresh()]] to invalidate all cached index schemas. + * @return string the cache group name + */ + protected function getCacheGroup() + { + return md5(serialize([ + __CLASS__, + $this->db->dsn, + $this->db->username, + ])); + } + + /** + * Returns the metadata for all indexes in the database. + * @param string $schema the schema of the indexes. Defaults to empty string, meaning the current or default schema name. + * @param boolean $refresh whether to fetch the latest available index schemas. If this is false, + * cached data may be returned if available. + * @return IndexSchema[] the metadata for all indexes in the Sphinx. + * Each array element is an instance of [[IndexSchema]] or its child class. + */ + public function getTableSchemas($schema = '', $refresh = false) + { + $indexes = []; + foreach ($this->getIndexNames($schema, $refresh) as $name) { + if ($schema !== '') { + $name = $schema . '.' . $name; + } + if (($index = $this->getIndexSchema($name, $refresh)) !== null) { + $indexes[] = $index; + } + } + return $indexes; + } + + /** + * Returns all index names in the database. + * @param string $schema the schema of the indexes. Defaults to empty string, meaning the current or default schema name. + * If not empty, the returned index names will be prefixed with the schema name. + * @param boolean $refresh whether to fetch the latest available index names. If this is false, + * index names fetched previously (if available) will be returned. + * @return string[] all index names in the database. + */ + public function getIndexNames($schema = '', $refresh = false) + { + if (!isset($this->_indexNames[$schema]) || $refresh) { + $this->_indexNames[$schema] = $this->findIndexNames($schema); + } + return $this->_indexNames[$schema]; + } + + /** + * Returns all index names in the database. + * @param string $schema the schema of the indexes. Defaults to empty string, meaning the current or default schema. + * @return array all index names in the database. The names have NO schema name prefix. + */ + protected function findIndexNames($schema = '') + { + $sql = 'SHOW TABLES'; + if ($schema !== '') { + $sql .= ' FROM ' . $this->quoteSimpleIndexName($schema); + } + return $this->db->createCommand($sql)->queryColumn(); + } + + /** + * @return QueryBuilder the query builder for this connection. + */ + public function getQueryBuilder() + { + if ($this->_builder === null) { + $this->_builder = $this->createQueryBuilder(); + } + return $this->_builder; + } + + /** + * Determines the PDO type for the given 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 + */ + public function getPdoType($data) + { + static $typeMap = [ + // php type => PDO type + 'boolean' => \PDO::PARAM_BOOL, + 'integer' => \PDO::PARAM_INT, + 'string' => \PDO::PARAM_STR, + 'resource' => \PDO::PARAM_LOB, + 'NULL' => \PDO::PARAM_NULL, + ]; + $type = gettype($data); + return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; + } + + /** + * Refreshes the schema. + * This method cleans up all cached index schemas so that they can be re-created later + * to reflect the Sphinx schema change. + */ + public function refresh() + { + /** @var $cache Cache */ + $cache = is_string($this->db->schemaCache) ? Yii::$app->getComponent($this->db->schemaCache) : $this->db->schemaCache; + if ($this->db->enableSchemaCache && $cache instanceof Cache) { + GroupDependency::invalidate($cache, $this->getCacheGroup()); + } + $this->_indexNames = []; + $this->_indexes = []; + } + + /** + * Creates a query builder for the Sphinx. * @return QueryBuilder query builder instance */ public function createQueryBuilder() @@ -45,31 +268,148 @@ class Schema extends \yii\db\mysql\Schema } /** - * Loads the metadata for the specified table. - * @param string $name table name - * @return TableSchema driver dependent table metadata. Null if the table does not exist. + * Quotes a string value for use in a query. + * Note that if the parameter is not a string, it will be returned without change. + * @param string $str string to be quoted + * @return string the properly quoted string + * @see http://www.php.net/manual/en/function.PDO-quote.php */ - protected function loadTableSchema($name) + public function quoteValue($str) + { + if (!is_string($str)) { + return $str; + } + + $this->db->open(); + if (($value = $this->db->pdo->quote($str)) !== false) { + return $value; + } else { // the driver doesn't support quote (e.g. oci) + return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'"; + } + } + + /** + * Quotes a index name for use in a query. + * If the index name contains schema prefix, the prefix will also be properly quoted. + * If the index name is already quoted or contains '(' or '{{', + * then this method will do nothing. + * @param string $name index name + * @return string the properly quoted index name + * @see quoteSimpleTableName + */ + public function quoteIndexName($name) + { + if (strpos($name, '(') !== false || strpos($name, '{{') !== false) { + return $name; + } + if (strpos($name, '.') === false) { + return $this->quoteSimpleIndexName($name); + } + $parts = explode('.', $name); + foreach ($parts as $i => $part) { + $parts[$i] = $this->quoteSimpleIndexName($part); + } + return implode('.', $parts); + } + + /** + * Quotes a column name for use in a query. + * If the column name contains prefix, the prefix will also be properly quoted. + * If the column name is already quoted or contains '(', '[[' or '{{', + * then this method will do nothing. + * @param string $name column name + * @return string the properly quoted column name + * @see quoteSimpleColumnName + */ + public function quoteColumnName($name) + { + if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) { + return $name; + } + if (($pos = strrpos($name, '.')) !== false) { + $prefix = $this->quoteIndexName(substr($name, 0, $pos)) . '.'; + $name = substr($name, $pos + 1); + } else { + $prefix = ''; + } + return $prefix . $this->quoteSimpleColumnName($name); + } + + /** + * Quotes a index name for use in a query. + * A simple index name has no schema prefix. + * @param string $name index name + * @return string the properly quoted index name + */ + public function quoteSimpleIndexName($name) + { + return strpos($name, "`") !== false ? $name : "`" . $name . "`"; + } + + /** + * Quotes a column name for use in a query. + * A simple column name has no prefix. + * @param string $name column name + * @return string the properly quoted column name + */ + public function quoteSimpleColumnName($name) { - $table = new TableSchema; - $this->resolveTableNames($table, $name); + return strpos($name, '`') !== false || $name === '*' ? $name : '`' . $name . '`'; + } - if ($this->findColumns($table)) { - return $table; + /** + * Returns the actual name of a given index name. + * This method will strip off curly brackets from the given index name + * and replace the percentage character '%' with [[Connection::indexPrefix]]. + * @param string $name the index name to be converted + * @return string the real name of the given index name + */ + public function getRawIndexName($name) + { + if (strpos($name, '{{') !== false) { + $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name); + return str_replace('%', $this->db->tablePrefix, $name); } else { - return null; + return $name; + } + } + + /** + * Extracts the PHP type from abstract DB type. + * @param ColumnSchema $column the column schema information + * @return string PHP type name + */ + protected function getColumnPhpType($column) + { + static $typeMap = [ // abstract type => php type + 'smallint' => 'integer', + 'integer' => 'integer', + 'bigint' => 'integer', + 'boolean' => 'boolean', + 'float' => 'double', + ]; + if (isset($typeMap[$column->type])) { + if ($column->type === 'bigint') { + return PHP_INT_SIZE == 8 ? 'integer' : 'string'; + } elseif ($column->type === 'integer') { + return PHP_INT_SIZE == 4 ? 'string' : 'integer'; + } else { + return $typeMap[$column->type]; + } + } else { + return 'string'; } } /** - * Collects the metadata of table columns. - * @param TableSchema $table the table metadata - * @return boolean whether the table exists in the database + * Collects the metadata of index columns. + * @param IndexSchema $index the index metadata + * @return boolean whether the index exists in the database * @throws \Exception if DB query fails */ - protected function findColumns($table) + protected function findColumns($index) { - $sql = 'DESCRIBE ' . $this->quoteSimpleTableName($table->name); + $sql = 'DESCRIBE ' . $this->quoteSimpleIndexName($index->name); try { $columns = $this->db->createCommand($sql)->queryAll(); } catch (\Exception $e) { @@ -82,9 +422,9 @@ class Schema extends \yii\db\mysql\Schema } foreach ($columns as $info) { $column = $this->loadColumnSchema($info); - $table->columns[$column->name] = $column; + $index->columns[$column->name] = $column; if ($column->isPrimaryKey) { - $table->primaryKey[] = $column->name; + $index->primaryKey[] = $column->name; } } return true;