/** * Construct the database dump command. * * @param \Drupal\Core\Database\Connection $connection * The database connection to use. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler to use. */ function __construct(Connection $connection, ModuleHandlerInterface $module_handler) { // Check this is MySQL. if ($connection->databaseType() !== 'mysql') { throw new \RuntimeException('This script can only be used with MySQL database backends.'); } $this->connection = $connection; $this->moduleHandler = $module_handler; parent::__construct(); }
/** * Returns a schema array for a given table. * * @param \Drupal\Core\Database\Connection $connection * The database connection to use. * @param string $table * The table name. * * @return array * A schema array (as defined by hook_schema()). * * @todo This implementation is hard-coded for MySQL. */ protected function getTableSchema(Connection $connection, $table) { // Check this is MySQL. if ($connection->databaseType() !== 'mysql') { throw new \RuntimeException('This script can only be used with MySQL database backends.'); } $query = $connection->query("SHOW FULL COLUMNS FROM {" . $table . "}"); $definition = []; while (($row = $query->fetchAssoc()) !== FALSE) { $name = $row['Field']; // Parse out the field type and meta information. preg_match('@([a-z]+)(?:\\((\\d+)(?:,(\\d+))?\\))?\\s*(unsigned)?@', $row['Type'], $matches); $type = $this->fieldTypeMap($connection, $matches[1]); if ($row['Extra'] === 'auto_increment') { // If this is an auto increment, then the type is 'serial'. $type = 'serial'; } $definition['fields'][$name] = ['type' => $type, 'not null' => $row['Null'] === 'NO']; if ($size = $this->fieldSizeMap($connection, $matches[1])) { $definition['fields'][$name]['size'] = $size; } if (isset($matches[2]) && $type === 'numeric') { // Add precision and scale. $definition['fields'][$name]['precision'] = $matches[2]; $definition['fields'][$name]['scale'] = $matches[3]; } elseif ($type === 'time' || $type === 'datetime') { // @todo Core doesn't support these, but copied from `migrate-db.sh` for now. // Convert to varchar. $definition['fields'][$name]['type'] = 'varchar'; $definition['fields'][$name]['length'] = '100'; } elseif (!isset($definition['fields'][$name]['size'])) { // Try use the provided length, if it doesn't exist default to 100. It's // not great but good enough for our dumps at this point. $definition['fields'][$name]['length'] = isset($matches[2]) ? $matches[2] : 100; } if (isset($row['Default'])) { $definition['fields'][$name]['default'] = $row['Default']; } if (isset($matches[4])) { $definition['fields'][$name]['unsigned'] = TRUE; } // Check for the 'varchar_ascii' type that should be 'binary'. if (isset($row['Collation']) && $row['Collation'] == 'ascii_bin') { $definition['fields'][$name]['type'] = 'varchar_ascii'; $definition['fields'][$name]['binary'] = TRUE; } // Check for the non-binary 'varchar_ascii'. if (isset($row['Collation']) && $row['Collation'] == 'ascii_general_ci') { $definition['fields'][$name]['type'] = 'varchar_ascii'; } // Check for the 'utf8_bin' collation. if (isset($row['Collation']) && $row['Collation'] == 'utf8_bin') { $definition['fields'][$name]['binary'] = TRUE; } } // Set primary key, unique keys, and indexes. $this->getTableIndexes($connection, $table, $definition); // Set table collation. $this->getTableCollation($connection, $table, $definition); return $definition; }
/** * Updates the storage tables when the field configuration changes. * * @param \Drupal\search_api\IndexInterface $index * The search index whose fields (might) have changed. * * @return bool * TRUE if the data needs to be reindexed, FALSE otherwise. * * @throws \Drupal\search_api\SearchApiException * Thrown if any exceptions occur internally, e.g., in the database * layer. */ protected function fieldsUpdated(IndexInterface $index) { try { $db_info = $this->getIndexDbInfo($index); $fields = &$db_info['field_tables']; $new_fields = $index->getFields(); $reindex = FALSE; $cleared = FALSE; $text_table = NULL; $denormalized_table = $db_info['index_table']; foreach ($fields as $field_id => $field) { if (!isset($text_table) && Utility::isTextType($field['type'])) { // Stash the shared text table name for the index. $text_table = $field['table']; } if (!isset($new_fields[$field_id])) { // The field is no longer in the index, drop the data. $this->removeFieldStorage($field_id, $field, $denormalized_table); unset($fields[$field_id]); continue; } $old_type = $field['type']; $new_type = $new_fields[$field_id]->getType(); $fields[$field_id]['type'] = $new_type; $fields[$field_id]['boost'] = $new_fields[$field_id]->getBoost(); if ($old_type != $new_type) { if ($old_type == 'text' || $new_type == 'text') { // A change in fulltext status necessitates completely clearing the // index. $reindex = TRUE; if (!$cleared) { $cleared = TRUE; $this->deleteAllIndexItems($index); } $this->removeFieldStorage($field_id, $field, $denormalized_table); // Keep the table in $new_fields to create the new storage. continue; } elseif ($this->sqlType($old_type) != $this->sqlType($new_type)) { // There is a change in SQL type. We don't have to clear the index, // since types can be converted. $this->database->schema()->changeField($field['table'], 'value', 'value', $this->sqlType($new_type) + array('description' => "The field's value for this item")); $this->database->schema()->changeField($denormalized_table, $field['column'], $field['column'], $this->sqlType($new_type) + array('description' => "The field's value for this item")); $reindex = TRUE; } elseif ($old_type == 'date' || $new_type == 'date') { // Even though the SQL type stays the same, we have to reindex since // conversion rules change. $reindex = TRUE; } } elseif ($new_type == 'text' && $field['boost'] != $new_fields[$field_id]->getBoost()) { if (!$reindex) { $multiplier = $new_fields[$field_id]->getBoost() / $field['boost']; $this->database->update($text_table) ->expression('score', 'score * :mult', array(':mult' => $multiplier)) ->condition('field_name', self::getTextFieldName($field_id)) ->execute(); } } // Make sure the table and column now exist. (Especially important when // we actually add the index for the first time.) $storage_exists = $this->database->schema() ->fieldExists($field['table'], 'value'); $denormalized_storage_exists = $this->database->schema() ->fieldExists($denormalized_table, $field['column']); if (!Utility::isTextType($field['type']) && !$storage_exists) { $db = array( 'table' => $field['table'], ); $this->createFieldTable($new_fields[$field_id], $db); } // Ensure that a column is created in the denormalized storage even for // 'text' fields. if (!$denormalized_storage_exists) { $db = array( 'table' => $denormalized_table, 'column' => $field['column'], ); $this->createFieldTable($new_fields[$field_id], $db); } unset($new_fields[$field_id]); } $prefix = 'search_api_db_' . $index->id(); // These are new fields that were previously not indexed. foreach ($new_fields as $field_id => $field) { $reindex = TRUE; if (Utility::isTextType($field->getType())) { if (!isset($text_table)) { // If we have not encountered a text table, assign a name for it. $text_table = $this->findFreeTable($prefix . '_', 'text'); } $fields[$field_id]['table'] = $text_table; } else { $fields[$field_id]['table'] = $this->findFreeTable($prefix . '_', $field_id); $this->createFieldTable($field, $fields[$field_id]); } // Always add a column in the denormalized table. $fields[$field_id]['column'] = $this->findFreeColumn($denormalized_table, $field_id); $this->createFieldTable($field, array('table' => $denormalized_table, 'column' => $fields[$field_id]['column'])); $fields[$field_id]['type'] = $field->getType(); $fields[$field_id]['boost'] = $field->getBoost(); } // If needed, make sure the text table exists. if (isset($text_table) && !$this->database->schema()->tableExists($text_table)) { $table = array( 'name' => $text_table, 'module' => 'search_api_db', 'fields' => array( 'item_id' => array( 'type' => 'varchar', 'length' => 50, 'description' => 'The primary identifier of the item', 'not null' => TRUE, ), 'field_name' => array( 'description' => "The name of the field in which the token appears, or an MD5 hash of the field", 'not null' => TRUE, 'type' => 'varchar', 'length' => 191, ), 'word' => array( 'description' => 'The text of the indexed token', 'type' => 'varchar', 'length' => 50, 'not null' => TRUE, ), 'score' => array( 'description' => 'The score associated with this token', 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, ), ), 'indexes' => array( 'word_field' => array(array('word', 20), 'field_name'), ), // Add a covering index since word is not repeated for each item. 'primary key' => array('item_id', 'field_name', 'word'), ); $this->database->schema()->createTable($text_table, $table); // Some DBMSs will need a character encoding and collation set. Since // this largely circumvents Drupal's database layer, but isn't integral // enough to fail completely when it doesn't work, we wrap it in a // try/catch, to be on the safe side. try { switch ($this->database->databaseType()) { case 'mysql': $this->database->query("ALTER TABLE {{$text_table}} CONVERT TO CHARACTER SET 'utf8' COLLATE 'utf8_bin'"); break; case 'pgsql': $this->database->query("ALTER TABLE {{$text_table}} ALTER COLUMN word SET DATA TYPE character varying(50) COLLATE \"C\""); break; // @todo Add fixes for other DBMSs. case 'oracle': case 'sqlite': case 'sqlsrv': break; } } catch (\PDOException $e) { $vars['%index'] = $index->label(); watchdog_exception('search_api_db', $e, '%type while trying to change collation for the fulltext table of index %index: @message in %function (line %line of %file).', $vars); } } $this->getKeyValueStore()->set($index->id(), $db_info); return $reindex; } // The database operations might throw PDO or other exceptions, so we catch // them all and re-wrap them appropriately. catch (\Exception $e) { throw new SearchApiException($e->getMessage(), $e->getCode(), $e); } }