/** * Saves a link without clearing caches. * * @param array $link * A definition, according to $definitionFields, for a * \Drupal\Core\Menu\MenuLinkInterface plugin. * * @return array * The menu names affected by the save operation. This will be one menu * name if the link is saved to the sane menu, or two if it is saved to a * new menu. * * @throws \Exception * Thrown if the storage back-end does not exist and could not be created. * @throws \Drupal\Component\Plugin\Exception\PluginException * Thrown if the definition is invalid, for example, if the specified parent * would cause the links children to be moved to greater than the maximum * depth. */ protected function doSave(array $link) { $original = $this->loadFull($link['id']); // @todo Should we just return here if the link values match the original // values completely? // https://www.drupal.org/node/2302137 $affected_menus = array(); $transaction = $this->connection->startTransaction(); try { if ($original) { $link['mlid'] = $original['mlid']; $link['has_children'] = $original['has_children']; $affected_menus[$original['menu_name']] = $original['menu_name']; } else { // Generate a new mlid. $options = array('return' => Database::RETURN_INSERT_ID) + $this->options; $link['mlid'] = $this->connection->insert($this->table, $options)->fields(array('id' => $link['id'], 'menu_name' => $link['menu_name']))->execute(); } $fields = $this->preSave($link, $original); // We may be moving the link to a new menu. $affected_menus[$fields['menu_name']] = $fields['menu_name']; $query = $this->connection->update($this->table, $this->options); $query->condition('mlid', $link['mlid']); $query->fields($fields)->execute(); if ($original) { $this->updateParentalStatus($original); } $this->updateParentalStatus($link); } catch (\Exception $e) { $transaction->rollback(); throw $e; } return $affected_menus; }
/** * {@inheritdoc} */ public function setMultiple(array $items) { // Use a transaction so that the database can write the changes in a single // commit. $transaction = $this->connection->startTransaction(); try { // Delete all items first so we can do one insert. Rather than multiple // merge queries. $this->deleteMultiple(array_keys($items)); $query = $this->connection->insert($this->bin)->fields(array('cid', 'data', 'expire', 'created', 'serialized', 'tags', 'checksum')); foreach ($items as $cid => $item) { $item += array('expire' => CacheBackendInterface::CACHE_PERMANENT, 'tags' => array()); Cache::validateTags($item['tags']); $item['tags'] = array_unique($item['tags']); // Sort the cache tags so that they are stored consistently in the DB. sort($item['tags']); $fields = array('cid' => $cid, 'expire' => $item['expire'], 'created' => round(microtime(TRUE), 3), 'tags' => implode(' ', $item['tags']), 'checksum' => $this->checksumProvider->getCurrentChecksum($item['tags'])); if (!is_string($item['data'])) { $fields['data'] = serialize($item['data']); $fields['serialized'] = 1; } else { $fields['data'] = $item['data']; $fields['serialized'] = 0; } $query->values($fields); } $query->execute(); } catch (\Exception $e) { $transaction->rollback(); // @todo Log something here or just re throw? throw $e; } }
/** * Dumps a set of routes to the router table in the database. * * Available options: * - provider: The route grouping that is being dumped. All existing * routes with this provider will be deleted on dump. * - base_class: The base class name. * * @param array $options * An array of options. */ public function dump(array $options = array()) { // Convert all of the routes into database records. // Accumulate the menu masks on top of any we found before. $masks = array_flip($this->state->get('routing.menu_masks.' . $this->tableName, array())); // Delete any old records first, then insert the new ones. That avoids // stale data. The transaction makes it atomic to avoid unstable router // states due to random failures. $transaction = $this->connection->startTransaction(); try { // We don't use truncate, because it is not guaranteed to be transaction // safe. try { $this->connection->delete($this->tableName)->execute(); } catch (\Exception $e) { $this->ensureTableExists(); } // Split the routes into chunks to avoid big INSERT queries. $route_chunks = array_chunk($this->routes->all(), 50, TRUE); foreach ($route_chunks as $routes) { $insert = $this->connection->insert($this->tableName)->fields(array('name', 'fit', 'path', 'pattern_outline', 'number_parts', 'route')); $names = array(); foreach ($routes as $name => $route) { /** @var \Symfony\Component\Routing\Route $route */ $route->setOption('compiler_class', '\\Drupal\\Core\\Routing\\RouteCompiler'); /** @var \Drupal\Core\Routing\CompiledRoute $compiled */ $compiled = $route->compile(); // The fit value is a binary number which has 1 at every fixed path // position and 0 where there is a wildcard. We keep track of all such // patterns that exist so that we can minimize the number of path // patterns we need to check in the RouteProvider. $masks[$compiled->getFit()] = 1; $names[] = $name; $values = array('name' => $name, 'fit' => $compiled->getFit(), 'path' => $route->getPath(), 'pattern_outline' => $compiled->getPatternOutline(), 'number_parts' => $compiled->getNumParts(), 'route' => serialize($route)); $insert->values($values); } // Insert all new routes. $insert->execute(); } } catch (\Exception $e) { $transaction->rollback(); watchdog_exception('Routing', $e); throw $e; } // Sort the masks so they are in order of descending fit. $masks = array_keys($masks); rsort($masks); $this->state->set('routing.menu_masks.' . $this->tableName, $masks); $this->routes = NULL; }
/** * {@inheritdoc} */ public function save(EntityInterface $entity) { $transaction = $this->database->startTransaction(); try { $return = parent::save($entity); // Ignore replica server temporarily. db_ignore_replica(); return $return; } catch (\Exception $e) { $transaction->rollback(); watchdog_exception($this->entityTypeId, $e); throw new EntityStorageException($e->getMessage(), $e->getCode(), $e); } }
/** * {@inheritdoc} */ public function setMultiple(array $items) { $deleted_tags =& drupal_static('Drupal\\Core\\Cache\\DatabaseBackend::deletedTags', array()); $invalidated_tags =& drupal_static('Drupal\\Core\\Cache\\DatabaseBackend::invalidatedTags', array()); // Use a transaction so that the database can write the changes in a single // commit. $transaction = $this->connection->startTransaction(); try { // Delete all items first so we can do one insert. Rather than multiple // merge queries. $this->deleteMultiple(array_keys($items)); $query = $this->connection->insert($this->bin)->fields(array('cid', 'data', 'expire', 'created', 'serialized', 'tags', 'checksum_invalidations', 'checksum_deletions')); foreach ($items as $cid => $item) { $item += array('expire' => CacheBackendInterface::CACHE_PERMANENT, 'tags' => array()); Cache::validateTags($item['tags']); $item['tags'] = array_unique($item['tags']); // Sort the cache tags so that they are stored consistently in the DB. sort($item['tags']); // Remove tags that were already deleted or invalidated during this // request from the static caches so that another deletion or // invalidation can occur. foreach ($item['tags'] as $tag) { if (isset($deleted_tags[$tag])) { unset($deleted_tags[$tag]); } if (isset($invalidated_tags[$tag])) { unset($invalidated_tags[$tag]); } } $checksum = $this->checksumTags($item['tags']); $fields = array('cid' => $cid, 'expire' => $item['expire'], 'created' => round(microtime(TRUE), 3), 'tags' => implode(' ', $item['tags']), 'checksum_invalidations' => $checksum['invalidations'], 'checksum_deletions' => $checksum['deletions']); if (!is_string($item['data'])) { $fields['data'] = serialize($item['data']); $fields['serialized'] = 1; } else { $fields['data'] = $item['data']; $fields['serialized'] = 0; } $query->values($fields); } $query->execute(); } catch (\Exception $e) { $transaction->rollback(); // @todo Log something here or just re throw? throw $e; } }
/** * {@inheritdoc} */ public function setMultiple(array $items) { $values = array(); foreach ($items as $cid => $item) { $item += array('expire' => CacheBackendInterface::CACHE_PERMANENT, 'tags' => array()); Cache::validateTags($item['tags']); $item['tags'] = array_unique($item['tags']); // Sort the cache tags so that they are stored consistently in the DB. sort($item['tags']); $fields = array('cid' => $cid, 'expire' => $item['expire'], 'created' => round(microtime(TRUE), 3), 'tags' => implode(' ', $item['tags']), 'checksum' => $this->checksumProvider->getCurrentChecksum($item['tags'])); if (!is_string($item['data'])) { $fields['data'] = serialize($item['data']); $fields['serialized'] = 1; } else { $fields['data'] = $item['data']; $fields['serialized'] = 0; } $values[] = $fields; } // Use a transaction so that the database can write the changes in a single // commit. The transaction is started after calculating the tag checksums // since that can create a table and this causes an exception when using // PostgreSQL. $transaction = $this->connection->startTransaction(); try { // Delete all items first so we can do one insert. Rather than multiple // merge queries. $this->deleteMultiple(array_keys($items)); $query = $this->connection->insert($this->bin)->fields(array('cid', 'expire', 'created', 'tags', 'checksum', 'data', 'serialized')); foreach ($values as $fields) { // Only pass the values since the order of $fields matches the order of // the insert fields. This is a performance optimization to avoid // unnecessary loops within the method. $query->values(array_values($fields)); } $query->execute(); } catch (\Exception $e) { $transaction->rollback(); // @todo Log something here or just re throw? throw $e; } }
/** * Updates the schema for a field stored in a shared table. * * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition * The storage definition of the field being updated. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original * The original storage definition; i.e., the definition before the update. * * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException * Thrown when the update to the field is forbidden. * @throws \Exception * Rethrown exception if the table recreation fails. */ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { if (!$this->storage->countFieldData($original, TRUE)) { if ($this->database->supportsTransactionalDDL()) { // If the database supports transactional DDL, we can go ahead and rely // on it. If not, we will have to rollback manually if something fails. $transaction = $this->database->startTransaction(); } try { // Since there is no data we may be switching from a dedicated table // to a schema table schema, hence we should use the proper API. $this->performFieldSchemaOperation('delete', $original); $this->performFieldSchemaOperation('create', $storage_definition); } catch (\Exception $e) { if ($this->database->supportsTransactionalDDL()) { $transaction->rollback(); } else { // Recreate original schema. $this->createSharedTableSchema($original); } throw $e; } } else { if ($this->hasColumnChanges($storage_definition, $original)) { throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.'); } $updated_field_name = $storage_definition->getName(); $table_mapping = $this->storage->getTableMapping(); $column_names = $table_mapping->getColumnNames($updated_field_name); $schema_handler = $this->database->schema(); // Iterate over the mapped table to find the ones that host the deleted // field schema. $original_schema = $this->loadFieldSchemaData($original); $schema = array(); foreach ($table_mapping->getTableNames() as $table_name) { foreach ($table_mapping->getFieldNames($table_name) as $field_name) { if ($field_name == $updated_field_name) { $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names); // Handle NOT NULL constraints. foreach ($schema[$table_name]['fields'] as $column_name => $specifier) { $not_null = !empty($specifier['not null']); $original_not_null = !empty($original_schema[$table_name]['fields'][$column_name]['not null']); if ($not_null !== $original_not_null) { if ($not_null && $this->hasNullFieldPropertyData($table_name, $column_name)) { throw new EntityStorageException("The {$column_name} column cannot have NOT NULL constraints as it holds NULL values."); } $column_schema = $original_schema[$table_name]['fields'][$column_name]; $column_schema['not null'] = $not_null; $schema_handler->changeField($table_name, $field_name, $field_name, $column_schema); } } // Drop original indexes and unique keys. if (!empty($original_schema[$table_name]['indexes'])) { foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) { $schema_handler->dropIndex($table_name, $name); } } if (!empty($original_schema[$table_name]['unique keys'])) { foreach ($original_schema[$table_name]['unique keys'] as $name => $specifier) { $schema_handler->dropUniqueKey($table_name, $name); } } // Create new indexes and unique keys. if (!empty($schema[$table_name]['indexes'])) { foreach ($schema[$table_name]['indexes'] as $name => $specifier) { // Check if the index exists because it might already have been // created as part of the earlier entity type update event. $this->addIndex($table_name, $name, $specifier, $schema[$table_name]); } } if (!empty($schema[$table_name]['unique keys'])) { foreach ($schema[$table_name]['unique keys'] as $name => $specifier) { $schema_handler->addUniqueKey($table_name, $name, $specifier); } } // After deleting the field schema skip to the next table. break; } } } $this->saveFieldSchemaData($storage_definition, $schema); } }
/** * {@inheritdoc} */ public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { if (!$storage_definition->hasData()) { // There is no data. Re-create the tables completely. if ($this->database->supportsTransactionalDDL()) { // If the database supports transactional DDL, we can go ahead and rely // on it. If not, we will have to rollback manually if something fails. $transaction = $this->database->startTransaction(); } try { $original_schema = $this->_fieldSqlSchema($original); foreach ($original_schema as $name => $table) { $this->database->schema()->dropTable($name, $table); } $schema = $this->_fieldSqlSchema($storage_definition); foreach ($schema as $name => $table) { $this->database->schema()->createTable($name, $table); } } catch (\Exception $e) { if ($this->database->supportsTransactionalDDL()) { $transaction->rollback(); } else { // Recreate tables. $original_schema = $this->_fieldSqlSchema($original); foreach ($original_schema as $name => $table) { if (!$this->database->schema()->tableExists($name)) { $this->database->schema()->createTable($name, $table); } } } throw $e; } } else { if ($storage_definition->getColumns() != $original->getColumns()) { throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data."); } // There is data, so there are no column changes. Drop all the prior // indexes and create all the new ones, except for all the priors that // exist unchanged. $table = static::_fieldTableName($original); $revision_table = static::_fieldRevisionTableName($original); $schema = $storage_definition->getSchema(); $original_schema = $original->getSchema(); foreach ($original_schema['indexes'] as $name => $columns) { if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) { $real_name = static::_fieldIndexName($storage_definition, $name); $this->database->schema()->dropIndex($table, $real_name); $this->database->schema()->dropIndex($revision_table, $real_name); } } $table = static::_fieldTableName($storage_definition); $revision_table = static::_fieldRevisionTableName($storage_definition); foreach ($schema['indexes'] as $name => $columns) { if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) { $real_name = static::_fieldIndexName($storage_definition, $name); $real_columns = array(); foreach ($columns as $column_name) { // Indexes can be specified as either a column name or an array with // column name and length. Allow for either case. if (is_array($column_name)) { $real_columns[] = array(static::_fieldColumnName($storage_definition, $column_name[0]), $column_name[1]); } else { $real_columns[] = static::_fieldColumnName($storage_definition, $column_name); } } $this->database->schema()->addIndex($table, $real_name, $real_columns); $this->database->schema()->addIndex($revision_table, $real_name, $real_columns); } } } }
/** * {@inheritdoc} */ public function save(EntityInterface $entity) { $transaction = $this->database->startTransaction(); try { // Sync the changes made in the fields array to the internal values array. $entity->updateOriginalValues(); $return = parent::save($entity); // Ignore replica server temporarily. db_ignore_replica(); return $return; } catch (\Exception $e) { $transaction->rollback(); watchdog_exception($this->entityTypeId, $e); throw new EntityStorageException($e->getMessage(), $e->getCode(), $e); } }
/** * Updates the schema for a field stored in a shared table. * * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition * The storage definition of the field being updated. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original * The original storage definition; i.e., the definition before the update. * * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException * Thrown when the update to the field is forbidden. * @throws \Exception * Rethrown exception if the table recreation fails. */ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { if (!$this->storage->countFieldData($original, TRUE)) { if ($this->database->supportsTransactionalDDL()) { // If the database supports transactional DDL, we can go ahead and rely // on it. If not, we will have to rollback manually if something fails. $transaction = $this->database->startTransaction(); } try { // Since there is no data we may be switching from a dedicated table // to a schema table schema, hence we should use the proper API. $this->performFieldSchemaOperation('delete', $original); $this->performFieldSchemaOperation('create', $storage_definition); } catch (\Exception $e) { if ($this->database->supportsTransactionalDDL()) { $transaction->rollback(); } else { // Recreate original schema. $this->createSharedTableSchema($original); } throw $e; } } else { if ($storage_definition->getColumns() != $original->getColumns()) { throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data."); } $updated_field_name = $storage_definition->getName(); $table_mapping = $this->storage->getTableMapping(); $column_names = $table_mapping->getColumnNames($updated_field_name); $schema_handler = $this->database->schema(); // Iterate over the mapped table to find the ones that host the deleted // field schema. $original_schema = $this->loadFieldSchemaData($original); $schema = array(); foreach ($table_mapping->getTableNames() as $table_name) { foreach ($table_mapping->getFieldNames($table_name) as $field_name) { if ($field_name == $updated_field_name) { $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names); // Drop original indexes and unique keys. if (!empty($original_schema[$table_name]['indexes'])) { foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) { $schema_handler->dropIndex($table_name, $name); } } if (!empty($original_schema[$table_name]['unique keys'])) { foreach ($original_schema[$table_name]['unique keys'] as $name => $specifier) { $schema_handler->dropUniqueKey($table_name, $name); } } // Create new indexes and unique keys. if (!empty($schema[$table_name]['indexes'])) { foreach ($schema[$table_name]['indexes'] as $name => $specifier) { $schema_handler->addIndex($table_name, $name, $specifier); } } if (!empty($schema[$table_name]['unique keys'])) { foreach ($schema[$table_name]['unique keys'] as $name => $specifier) { $schema_handler->addUniqueKey($table_name, $name, $specifier); } } // After deleting the field schema skip to the next table. break; } } } $this->saveFieldSchemaData($storage_definition, $schema); } }
/** * Indexes a single item on the specified index. * * Used as a helper method in indexItems(). * * @param \Drupal\search_api\IndexInterface $index * The index for which the item is being indexed. * @param string $id * The item's ID. * @param \Drupal\search_api\Item\ItemInterface $item * The item to index. * * @throws \Exception * Any encountered database (or other) exceptions are passed on, out of this * method. */ protected function indexItem(IndexInterface $index, $id, ItemInterface $item) { $fields = $this->getFieldInfo($index); $fields_updated = FALSE; $field_errors = array(); $db_info = $this->getIndexDbInfo($index); $denormalized_table = $db_info['index_table']; $txn = $this->database->startTransaction('search_api_indexing'); $text_table = $denormalized_table . '_text'; try { $inserts = array(); $text_inserts = array(); foreach ($item->getFields() as $name => $field) { $denormalized_value = NULL; // Sometimes index changes are not triggering the update hooks // correctly. Therefore, to avoid DB errors, we re-check the tables // here before indexing. if (empty($fields[$name]['table']) && !$fields_updated) { unset($db_info['field_tables'][$name]); $this->fieldsUpdated($index); $fields_updated = TRUE; $fields = $db_info['field_tables']; } if (empty($fields[$name]['table']) && empty($field_errors[$name])) { // Log an error, but only once per field. Since a superfluous field is // not too serious, we just index the rest of the item normally. $field_errors[$name] = TRUE; $this->getLogger()->warning("Unknown field @field: please check (and re-save) the index's fields settings.", array('@field' => $name)); continue; } $table = $fields[$name]['table']; $boost = $fields[$name]['boost']; $this->database->delete($table) ->condition('item_id', $id) ->execute(); $this->database->delete($denormalized_table) ->condition('item_id', $id) ->execute(); $type = $field->getType(); $value = array(); foreach ($field->getValues() as $field_value) { $converted_value = $this->convert($field_value, $type, $field->getOriginalType(), $index); // Don't add NULL values to the return array. Also, adding an empty // array is, of course, a waste of time. if (isset($converted_value) && $converted_value !== array()) { $value = array_merge($value, is_array($converted_value) ? $converted_value : array($converted_value)); } } if (Utility::isTextType($type, array('text', 'tokenized_text'))) { $words = array(); // Store the first 30 characters of the string as the denormalized // value. $field_value = $value; $denormalized_value = ''; do { $denormalized_value .= array_shift($field_value)['value'] . ' '; } while (strlen($denormalized_value) < 30); $denormalized_value = Unicode::truncateBytes(trim($denormalized_value), 30); foreach ($value as $token) { // Taken from core search to reflect less importance of words later // in the text. // Focus is a decaying value in terms of the amount of unique words // up to this point. From 100 words and more, it decays, to e.g. 0.5 // at 500 words and 0.3 at 1000 words. $focus = min(1, .01 + 3.5 / (2 + count($words) * .015)); $value = $token['value']; if (is_numeric($value)) { $value = ltrim($value, '-0'); } elseif (Unicode::strlen($value) < $this->configuration['min_chars']) { continue; } $value = Unicode::strtolower($value); $token['score'] = $token['score'] * $focus; if (!isset($words[$value])) { $words[$value] = $token; } else { $words[$value]['score'] += $token['score']; } $token['value'] = $value; } if ($words) { $field_name = self::getTextFieldName($name); foreach ($words as $word) { $text_inserts[$text_table][] = array( 'item_id' => $id, 'field_name' => $field_name, 'word' => $word['value'], 'score' => (int) round($word['score'] * $boost * self::SCORE_MULTIPLIER), ); } } } else { $values = array(); if (is_array($value)) { foreach ($value as $v) { if (isset($v)) { $values["$v"] = TRUE; } } $values = array_keys($values); } elseif (isset($value)) { $values[] = $value; } if ($values) { $denormalized_value = reset($values); $insert = $this->database->insert($table) ->fields(array('item_id', 'value')); foreach ($values as $v) { $insert->values(array( 'item_id' => $id, 'value' => $v, )); } $insert->execute(); } } // Insert a value in the denormalized table for all fields. if (isset($denormalized_value)) { $inserts[$denormalized_table][$fields[$name]['column']] = trim($denormalized_value); } } foreach ($inserts as $table => $data) { $this->database->insert($table) ->fields(array_merge($data, array('item_id' => $id))) ->execute(); } foreach ($text_inserts as $table => $data) { $query = $this->database->insert($table) ->fields(array('item_id', 'field_name', 'word', 'score')); foreach ($data as $row) { $query->values($row); } $query->execute(); } } catch (\Exception $e) { $txn->rollback(); throw $e; } }
/** * Indexes a single item on the specified index. * * Used as a helper method in indexItems(). * * @param \Drupal\search_api\IndexInterface $index * The index for which the item is being indexed. * @param \Drupal\search_api\Item\ItemInterface $item * The item to index. * * @throws \Exception * Any encountered database (or other) exceptions are passed on, out of this * method. */ protected function indexItem(IndexInterface $index, ItemInterface $item) { $fields = $this->getFieldInfo($index); $fields_updated = FALSE; $field_errors = array(); $db_info = $this->getIndexDbInfo($index); $denormalized_table = $db_info['index_table']; $item_id = $item->getId(); $transaction = $this->database->startTransaction('search_api_db_indexing'); try { // Remove the item from the denormalized table. $this->database->delete($denormalized_table)->condition('item_id', $item_id)->execute(); $denormalized_values = array(); $text_inserts = array(); foreach ($item->getFields() as $field_id => $field) { // Sometimes index changes are not triggering the update hooks // correctly. Therefore, to avoid DB errors, we re-check the tables // here before indexing. if (empty($fields[$field_id]['table']) && !$fields_updated) { unset($db_info['field_tables'][$field_id]); $this->fieldsUpdated($index); $fields_updated = TRUE; $fields = $db_info['field_tables']; } if (empty($fields[$field_id]['table']) && empty($field_errors[$field_id])) { // Log an error, but only once per field. Since a superfluous field is // not too serious, we just index the rest of the item normally. $field_errors[$field_id] = TRUE; $this->getLogger()->warning("Unknown field @field: please check (and re-save) the index's fields settings.", array('@field' => $field_id)); continue; } $field_info = $fields[$field_id]; $table = $field_info['table']; $column = $field_info['column']; $this->database->delete($table)->condition('item_id', $item_id)->execute(); $type = $field->getType(); $values = array(); foreach ($field->getValues() as $field_value) { $converted_value = $this->convert($field_value, $type, $field->getOriginalType(), $index); // Don't add NULL values to the return array. Also, adding an empty // array is, of course, a waste of time. if (isset($converted_value) && $converted_value !== array()) { $values = array_merge($values, is_array($converted_value) ? $converted_value : array($converted_value)); } } if (!$values) { // SQLite sometimes has problems letting columns not present in an // INSERT statement default to NULL, so we set NULL values for the // denormalized table explicitly. $denormalized_values[$column] = NULL; continue; } // If the field contains more than one value, we remember that the field // can be multi-valued. if (count($values) > 1) { $db_info['field_tables'][$field_id]['multi-valued'] = TRUE; } if (Utility::isTextType($type, array('text', 'tokenized_text'))) { // Remember the text table the first time we encounter it. if (!isset($text_table)) { $text_table = $table; } $unique_tokens = array(); $denormalized_value = ''; foreach ($values as $token) { $word = $token['value']; $score = $token['score']; // Store the first 30 characters of the string as the denormalized // value. if (strlen($denormalized_value) < 30) { $denormalized_value .= $word . ' '; } // Skip words that are too short, except for numbers. if (is_numeric($word)) { $word = ltrim($word, '-0'); } elseif (Unicode::strlen($word) < $this->configuration['min_chars']) { continue; } // Taken from core search to reflect less importance of words later // in the text. // Focus is a decaying value in terms of the amount of unique words // up to this point. From 100 words and more, it decays, to e.g. 0.5 // at 500 words and 0.3 at 1000 words. $score *= min(1, 0.01 + 3.5 / (2 + count($unique_tokens) * 0.015)); // Only insert each canonical base form of a word once. $word_base_form = $this->dbmsCompatibility->preprocessIndexValue($word); if (!isset($unique_tokens[$word_base_form])) { $unique_tokens[$word_base_form] = array('value' => $word, 'score' => $score); } else { $unique_tokens[$word_base_form]['score'] += $score; } } $denormalized_values[$column] = Unicode::truncateBytes(trim($denormalized_value), 30); if ($unique_tokens) { $field_name = self::getTextFieldName($field_id); $boost = $field_info['boost']; foreach ($unique_tokens as $token) { $text_inserts[] = array('item_id' => $item_id, 'field_name' => $field_name, 'word' => $token['value'], 'score' => (int) round($token['score'] * $boost * self::SCORE_MULTIPLIER)); } } } else { $denormalized_values[$column] = reset($values); // Make sure no duplicate values are inserted (which would lead to a // database exception). // Use the canonical base form of the value for the comparison to // avoid not catching different values that are duplicates under the // database table's collation. $case_insensitive_unique_values = array(); foreach ($values as $value) { $value_base_form = $this->dbmsCompatibility->preprocessIndexValue("{$value}", 'field'); // We still insert the value in its original case. $case_insensitive_unique_values[$value_base_form] = $value; } $values = array_values($case_insensitive_unique_values); $insert = $this->database->insert($table)->fields(array('item_id', 'value')); foreach ($values as $value) { $insert->values(array('item_id' => $item_id, 'value' => $value)); } $insert->execute(); } } $this->database->insert($denormalized_table)->fields(array_merge($denormalized_values, array('item_id' => $item_id)))->execute(); if ($text_inserts && isset($text_table)) { $query = $this->database->insert($text_table)->fields(array('item_id', 'field_name', 'word', 'score')); foreach ($text_inserts as $row) { $query->values($row); } $query->execute(); } // In case any new fields were detected as multi-valued, we re-save the // index's DB info. $this->getKeyValueStore()->set($index->id(), $db_info); } catch (\Exception $e) { $transaction->rollback(); throw $e; } }