Commit b0531076 authored by webchick's avatar webchick

#706842 by bangpound, bjaspan, Damien Tournoud, chx, aaronbauman: Fixed 'taxo...

#706842 by bangpound, bjaspan, Damien Tournoud, chx, aaronbauman: Fixed 'taxo as field' update broken + wipes some node/term associations .
parent 14d65ce1
......@@ -40,3 +40,4 @@ files[] = tests/update.test
files[] = tests/xmlrpc.test
files[] = tests/upgrade/upgrade.test
files[] = tests/upgrade/upgrade.poll.test
files[] = tests/upgrade/upgrade.taxonomy.test
<?php
// $Id$
/**
* Test taxonomy upgrades.
*/
class UpgradePathTaxonomyTestCase extends UpgradePathTestCase {
public static function getInfo() {
return array(
'name' => 'Taxonomy upgrade path',
'description' => 'Taxonomy upgrade path tests.',
'group' => 'Upgrade path',
);
}
public function setUp() {
// Path to the database dump.
$this->databaseDumpFile = drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.filled.database.php';
parent::setUp();
}
/**
* Retrieve an array mapping allowed vocabulary id to field name for
* all taxonomy_term_reference fields for which an instance exists
* for the specified entity type and bundle.
*/
function instanceVocabularies($entity_type, $bundle) {
$instances = array();
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field = field_info_field($instance['field_name']);
if ($field['type'] == 'taxonomy_term_reference') {
foreach ($field['settings']['allowed_values'] as $tree) {
// Prefer valid taxonomy term reference fields for a given vocabulary
// when they exist.
if (empty($instances[$tree['vid']]) || $instances[$tree['vid']] == 'taxonomyextra') {
$instances[$tree['vid']] = $field['field_name'];
}
}
}
}
return $instances;
}
/**
* Basic tests for the taxonomy upgrade.
*/
public function testTaxonomyUpgrade() {
$this->assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.'));
// Visit the front page to assert for PHP warning and errors.
$this->drupalGet('');
// Check that taxonomy_vocabulary_node_type and taxonomy_term_node have been
// removed.
$this->assertFalse(db_table_exists('taxonomy_vocabulary_node_type'), t('taxonomy_vocabulary_node_type has been removed.'));
$this->assertFalse(db_table_exists('taxonomy_term_node'), t('taxonomy_term_node has been removed.'));
// Check that the node type 'page' has been associated to a taxonomy
// reference field for each vocabulary.
$vocabularies = taxonomy_get_vocabularies();
$voc_keys = array_keys($vocabularies);
$instances = $this->instanceVocabularies('node', 'page');
$inst_keys = array_keys($instances);
sort($voc_keys);
sort($inst_keys);
$this->assertEqual($voc_keys, $inst_keys, t('Node type page has instances for every vocabulary.'));
// Node type 'story' was not explicitly in $vocabulary->nodes but
// each node of type 'story' was associated to one or more terms.
// Check that the node type 'story' has been associated only to
// the taxonomyextra field.
$instances = $this->instanceVocabularies('node', 'story');
$field_names = array_flip($instances);
$this->assertEqual(count($field_names), 1, t('Only one taxonomy term field instance exists for story nodes'));
$this->assertEqual(key($field_names), 'taxonomyextra', t('Only the excess taxonomy term field is used on story nodes'));
// Check that the node type 'poll' has been associated to no taxonomy
// reference field.
$instances = $this->instanceVocabularies('node', 'poll');
$this->assertTrue(empty($instances), t('Node type poll has no taxonomy term reference field instances.'));
// Check that each node of type 'page' and 'story' is associated to all the
// terms except terms whose ID is equal to the node ID or is equal to the
// node ID subtracted from 49.
$nodes = node_load_multiple(array(), array('type' => 'page'));
$nodes += node_load_multiple(array(), array('type' => 'story'));
$terms = db_select('taxonomy_term_data', 'td')
->fields('td')
->execute()
->fetchAllAssoc('tid');
field_attach_prepare_view('node', $nodes, 'full');
foreach ($nodes as $nid => $node) {
$node->content = field_attach_view('node', $node, 'full');
$render = drupal_render($node->content);
$this->drupalSetContent($render);
debug("Testing node $nid");
$this->verbose($render);
foreach ($terms as $tid => $term) {
$args = array(
'%name' => $term->name,
'@tid' => $tid,
'%nid' => $nid,
);
// Use link rather than term name because migrated term names can be
// substrings of other term names. e.g. "term 1 of vocabulary 2" is
// found when "term 1 of vocabulary 20" is output.
$link = l($term->name, 'taxonomy/term/' . $term->tid);
if (($tid == $nid) || ($tid + $nid == 49)) {
$this->assertNoRaw($link, t('Term %name (@tid) is not displayed on node %nid', $args));
}
else {
$this->assertRaw($link, t('Term %name (@tid) is displayed on node %nid', $args));
}
}
// The first 12 nodes have two revisions. For nodes with
// revisions, check that the oldest revision is associated only
// to terms whose ID is equal to the node ID or 49 less the node ID.
$revisions = node_revision_list($node);
if ($node->nid < 13) {
$this->assertEqual(count($revisions), 2, t('Node %nid has two revisions.', $args));
$last_rev = end($revisions);
$args['%old_vid'] = $last_rev->vid;
$node_old = node_load($node->nid, $last_rev->vid);
field_attach_prepare_view('node', array($node_old->nid => $node_old), 'full');
$node_old->content = field_attach_view('node', $node_old, 'full');
$render = drupal_render($node_old->content);
$this->drupalSetContent($render);
$this->verbose($render);
$term = $terms[$node->nid];
$link = l($term->name, 'taxonomy/term/' . $term->tid);
$this->assertRaw($link, t('Term %name (@tid) is displayed on node %nid vid %old_vid.', $args));
$term = $terms[49-$node->nid];
$link = l($term->name, 'taxonomy/term/' . $term->tid);
$this->assertRaw($link, t('Term %name (@tid) is displayed on node %nid %old_vid.', $args));
}
else {
$this->assertEqual(count($revisions), 1, t('Node %nid has one revision.', $args));
}
}
}
}
......@@ -430,7 +430,57 @@ function taxonomy_update_7004() {
field_create_instance($instance);
}
}
db_drop_table('taxonomy_vocabulary_node_type');
// Some contrib projects stored term node associations without regard for the
// selections in the taxonomy_vocabulary_node_types table, or have more terms
// for a single node than the vocabulary allowed. We construct the
// taxonomyextra field to store all the extra stuff.
// Allowed values for this extra vocabs field is every vocabulary.
$allowed_values = array();
foreach (taxonomy_get_vocabularies() as $vocabulary) {
$allowed_values[] = array(
'vid' => $vocabulary->vid,
'parent' => 0,
);
}
$field_name = 'taxonomyextra';
$field = array(
'field_name' => $field_name,
'type' => 'taxonomy_term_reference',
'cardinality' => FIELD_CARDINALITY_UNLIMITED,
'settings' => array(
'required' => FALSE,
'allowed_values' => $allowed_values,
),
);
field_create_field($field);
foreach (node_type_get_types() as $bundle) {
$instance = array(
'label' => 'Taxonomy upgrade extras',
'field_name' => $field_name,
'bundle' => $bundle->type,
'entity_type' => 'node',
'description' => 'Debris left over after upgrade from Drupal 6',
'widget' => array(
'type' => 'taxonomy_autocomplete',
),
'display' => array(
'default' => array(
'type' => 'taxonomy_term_reference_link',
'weight' => 10,
),
'teaser' => array(
'type' => 'taxonomy_term_reference_link',
'weight' => 10,
),
),
);
field_create_instance($instance);
}
$fields = array('help', 'multiple', 'required', 'tags');
foreach ($fields as $field) {
db_drop_field('taxonomy_vocabulary', $field);
......@@ -439,16 +489,46 @@ function taxonomy_update_7004() {
/**
* Migrate {taxonomy_term_node} table to field storage.
*
* @todo: This function can possibly be made much faster by wrapping a
* transaction around all the inserts.
*/
function taxonomy_update_7005(&$sandbox) {
// Since we are upgrading from Drupal 6, we know that only
// field_sql_storage.module will be enabled.
$field = field_info_field($field['field_name']);
$data_table = _field_sql_storage_tablename($field);
$revision_table = _field_sql_storage_revision_tablename($field);
$etid = _field_sql_storage_etid('node');
$value_column = $field['field_name'] . '_value';
$columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta', $value_column);
// $sandbox contents:
// - total: The total number of term_node relationships to migrate.
// - count: The number of term_node relationships that have been
// migrated so far.
// - last: The db_query_range() offset to use when querying
// term_node; this field is incremented in quantities of $batch
// (1000) but at the end of each call to this function, last and
// count are the same.
// - vocabularies: An associative array mapping vocabulary id and node
// type to field name. If a voc id/node type pair does not appear
// in this array but a term_node relationship exists mapping a
// term in voc id to node of that type, the relationship is
// assigned to the taxonomymyextra field which allows terms of all
// vocabularies.
// - cursor[values], cursor[deltas]: The contents of $values and
// $deltas at the end of the previous call to this function. These
// need to be preserved across calls because a single batch of
// 1000 rows from term_node may end in the middle of the terms for
// a single node revision.
//
// $values is the array of values about to be/most recently inserted
// into the SQL data table for the taxonomy_term_reference
// field. Before $values is constructed for each record, the
// $values from the previous insert is checked to see if the two
// records are for the same node revision id; this enables knowing
// when to reset the delta counters which are incremented across all
// terms for a single field on a single revision, but reset for each
// new field and revision.
//
// $deltas is an associative array mapping field name to the number
// of term references stored so far for the current revision, which
// provides the delta value for each term reference data insert. The
// deltas are reset for each new revision.
$field_info = field_info_fields();
// This is a multi-pass update. On the first call we need to initialize some
// variables.
......@@ -458,47 +538,164 @@ function taxonomy_update_7005(&$sandbox) {
$query = db_select('taxonomy_term_node', 't');
$sandbox['total'] = $query->countQuery()->execute()->fetchField();
$found = (bool) $sandbox['total'];
}
else {
// We do each pass in batches of 1000, this should result in a
// maximum of 2000 insert queries each operation.
$batch = 1000 + $sandbox['last'];
// Query and save data for the current revision.
$result = db_query_range('SELECT td.tid, tn.nid, td.weight, tn.vid, n2.type, n2.created, n2.sticky FROM {taxonomy_term_data} td INNER JOIN {taxonomy_term_node} tn ON td.tid = tn.tid INNER JOIN {node} n2 ON tn.nid = n2.nid INNER JOIN {node} n ON tn.vid = n.vid AND td.vid = :vocabulary_id ORDER BY td.weight ASC', array(':vocabulary_id' => $vocabulary->vid), $sandbox['last'], $batch);
$deltas = array();
// Use an inline version of Drupal 6 taxonomy_get_vocabularies() here since
// we can no longer rely on $vocabulary->nodes from the API function.
$result = db_query('SELECT v.vid, v.machine_name, n.type FROM {taxonomy_vocabulary} v INNER JOIN {taxonomy_vocabulary_node_type} n ON v.vid = n.vid');
$vocabularies = array();
foreach ($result as $record) {
$found = TRUE;
$sandbox['count'] += 1;
// Start deltas from 0, and increment by one for each
// term attached to a node.
$deltas[$record->nid] = isset($deltas[$record->nid]) ? ++$deltas[$record->nid] : 0;
$values = array($etid, $record->nid, $record->vid, $record->type, $deltas[$record->nid], $record->tid);
db_insert($data_table)->fields($columns)->values($values)->execute();
// Update the {taxonomy_index} table.
db_insert('taxonomy_index')
->fields(array('nid', 'tid', 'sticky', 'created',))
->values(array($record->nid, $record->tid, $record->sticky, $record->created))
->execute();
// If no node types are associated with a vocabulary, the LEFT JOIN will
// return a NULL value for type.
if (isset($record->type)) {
$vocabularies[$record->vid][$record->type] = 'taxonomy_'. $record->machine_name;
}
}
// Query and save data for all revisions.
$result = db_query('SELECT td.tid, tn.nid, td.weight, tn.vid, n.type FROM {taxonomy_term_data} td INNER JOIN {taxonomy_term_node} tn ON td.tid = tn.tid AND td.vid = :vocabulary_id INNER JOIN {node} n ON tn.nid = n.nid ORDER BY td.weight ASC', array(':vocabulary_id' => $vocabulary->vid), $sandbox['last'][$batch]);
$deltas = array();
if (!empty($vocabularies)) {
$sandbox['vocabularies'] = $vocabularies;
}
}
else {
$etid = _field_sql_storage_etid('node');
// We do each pass in batches of 1000.
$batch = 1000;
// Query selects all revisions at once and processes them in revision and
// term weight order. Join types:
//
// - INNER JOIN term_node ON tn.tid: We are migrating term-node
// relationships. If there are none for a term, we do not need the
// term_data row.
// - INNER JOIN {node} n ON n.nid: If a term-node relationship exists for a
// nid that does not exist, we cannot migrate it as we have no node to
// relate it to; thus we do not need that row from term_node.
// - LEFT JOIN {node} n2 ON n2.vid: If the current term-node relationship
// is for the current revision of the node, this left join will match and
// is_current will be non-NULL (we also get the current sticky and
// created in this case). This tells us whether to insert into the
// current data tables in addition to the revision data tables.
//
// This query must return a consistent ordering across multiple calls. We
// need them ordered by node vid (since we use that to decide when to reset
// the delta counters) and by term weight so they appear within each node
// in weight order. However, tn.vid,td.weight is not guaranteed to be
// unique, so we add tn.tid as an additional sort key because tn.tid,tn.vid
// is the primary key of the D6 term_node table and so is guaranteed
// unique. Unfortunately it also happens to be in the wrong order which is
// less efficient, but c'est la vie.
$query = 'SELECT td.vid AS vocab_id, td.tid, tn.nid, tn.vid, n.type, n2.created, n2.sticky, n2.nid AS is_current FROM {taxonomy_term_data} td INNER JOIN {taxonomy_term_node} tn ON td.tid = tn.tid INNER JOIN {node} n ON tn.nid = n.nid LEFT JOIN {node} n2 ON tn.vid = n2.vid ORDER BY tn.vid, td.weight ASC, tn.tid';
$result = db_query_range($query, $sandbox['last'], $batch);
if (isset($sandbox['cursor'])) {
$values = $sandbox['cursor']['values'];
$deltas = $sandbox['cursor']['deltas'];
}
else {
$deltas = array();
}
foreach ($result as $record) {
$found = TRUE;
$sandbox['count'] += 1;
// Start deltas at 0, and increment by one for each term attached to a revision.
$deltas[$record->vid] = isset($deltas[$record->vid]) ? ++$deltas[$record->vid] : 0;
$values = array($etid, $record->nid, $record->vid, $record->type, $deltas[$record->vid], $record->tid);
db_insert($revision_table)->fields($columns)->values($values)->execute();
// Use the valid field for this vocabulary and node type or use the
// overflow vocabulary if there is no valid field.
$field_name = isset($sandbox['vocabularies'][$record->vocab_id][$record->type]) ? $sandbox['vocabularies'][$record->vocab_id][$record->type] : 'taxonomyextra';
$field = $field_info[$field_name];
// Start deltas from 0, and increment by one for each term attached to a
// node.
if (!isset($deltas[$field_name])) {
$deltas[$field_name] = 0;
}
if (isset($values)) {
// If the last inserted revision_id is the same as the current record,
// use the previous deltas to calculate the next delta.
if ($record->vid == $values[2]) {
// see field_default_validate().
if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ($deltas[$field_name] + 1) >= $field['cardinality']) {
// For excess values of a single-term vocabulary, switch over to
// the overflow field.
$field_name = 'taxonomyextra';
$field = $field_info[$field_name];
if (!isset($deltas[$field_name])) {
$deltas[$field_name] = 0;
}
}
}
else {
// When the record is a new revision, empty the deltas array.
$deltas = array($field_name => 0);
}
}
// Table and column found in the field's storage details. During upgrades,
// it's always SQL.
$table = key($field['storage']['details']['sql'][FIELD_LOAD_REVISION]);
$value_column = $field['storage']['details']['sql'][FIELD_LOAD_REVISION][$table]['tid'];
// Column names and values in field storage are the same for current and
// revision.
$columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'language', 'delta', $value_column);
$values = array($etid, $record->nid, $record->vid, $record->type, LANGUAGE_NONE, $deltas[$field_name]++, $record->tid);
// Insert rows into the revision table.
db_insert($table)->fields($columns)->values($values)->execute();
// is_current column is a node ID if this revision is also current.
if ($record->is_current) {
$table = key($field['storage']['details']['sql'][FIELD_LOAD_CURRENT]);
db_insert($table)->fields($columns)->values($values)->execute();
// Update the {taxonomy_index} table.
db_insert('taxonomy_index')
->fields(array('nid', 'tid', 'sticky', 'created',))
->values(array($record->nid, $record->tid, $record->sticky, $record->created))
->execute();
}
}
$sandbox['last'] = $batch;
// Store the set of inserted values and the current revision's deltas in the
// sandbox.
$sandbox['cursor'] = array(
'values' => $values,
'deltas' => $deltas,
);
$sandbox['last'] += $batch;
}
if ($sandbox['count'] < $sandbox['total']) {
$sandbox['#finished'] = FALSE;
}
if (!$found) {
db_drop_table('taxonomy_term_node');
else {
db_drop_table('taxonomy_vocabulary_node_type');
db_drop_table('taxonomy_term_node');
// If there are no vocabs, we're done.
$sandbox['#finished'] = TRUE;
// Determine necessity of taxonomyextras field.
$field = $field_info['taxonomyextra'];
$table = key($field['storage']['details']['sql'][FIELD_LOAD_REVISION]);
$node_types = db_select($table)->distinct()->fields($table, array('bundle'))
->execute()->fetchCol();
if (empty($node_types)) {
// Delete the overflow field if there are no rows in the revision table.
field_delete_field('taxonomyextra');
}
else {
// Remove instances which are not actually used.
$bundles = array_diff($field['bundles']['node'], $node_types);
foreach ($bundles as $bundle) {
$instance = field_info_instance('node', 'taxonomyextra', $bundle);
field_delete_instance($instance);
}
}
}
}
......
......@@ -61,9 +61,12 @@
$multiple = array(0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1);
$required = array(0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1);
$voc_id = 0;
$term_id = 0;
for ($i = 0; $i < 24; $i++) {
$vocabulary = array();
$vocabulary['name'] = "vocabulary $i";
++$voc_id;
$vocabulary['name'] = "vocabulary $voc_id (i=$i)";
$vocabulary['description'] = "description of ". $vocabulary['name'];
$vocabulary['nodes'] = $i > 11 ? array('page' => TRUE) : array();
$vocabulary['multiple'] = $multiple[$i % 12];
......@@ -82,7 +85,8 @@
// For multiple parent vocabularies, omit the t0-t1 relation, otherwise
// every parent in the vocabulary is a parent.
$term['parent'] = $vocabulary['hierarchy'] == 2 && i == 1 ? array() : $parents;
$term['name'] = "term $j of vocabulary $i";
++$term_id;
$term['name'] = "term $term_id of vocabulary $voc_id (j=$j)";
$term['description'] = 'description of ' . $term['name'];
$term['weight'] = $i * 3 + $j;
taxonomy_save_term($term);
......@@ -91,6 +95,8 @@
}
}
$node_id = 0;
$revision_id = 0;
module_load_include('inc', 'node', 'node.pages');
for ($i = 0; $i < 24; $i++) {
$uid = intval($i / 8) + 3;
......@@ -99,7 +105,9 @@
$node->uid = $uid;
$node->type = $i < 12 ? 'page' : 'story';
$node->sticky = 0;
$node->title = "node title $i";
++$node_id;
++$revision_id;
$node->title = "node title $node_id rev $revision_id (i=$i)";
$type = node_get_types('type', $node->type);
if ($type->has_body) {
$node->body = str_repeat("node body ($node->type) - $i", 100);
......@@ -113,16 +121,26 @@
$node->promote = $i % 2;
$node->created = $now + $i * 86400;
$node->log = "added $i node";
$node->taxonomy = $terms;
// Just make every term association different a little.
unset($node->taxonomy[$i], $node->taxonomy[47 - $i]);
// Make every term association different a little. For nodes with revisions,
// make the initial revision have a different set of terms than the
// newest revision.
$node_terms = $terms;
unset($node_terms[$i], $node_terms[47 - $i]);
if ($node->revision) {
$node->taxonomy = array($i => $terms[$i], 47-$i => $terms[47 - $i]);
}
else {
$node->taxonomy = $node_terms;
}
node_save($node);
path_set_alias("node/$node->nid", "content/$node->created");
if ($node->revision) {
$user = user_load($uid + 3);
$node->title .= ' revision';
++$revision_id;
$node->title .= " rev2 $revision_id";
$node->body = str_repeat("node revision body ($node->type) - $i", 100);
$node->log = "added $i revision";
$node->taxonomy = $node_terms;
node_save($node);
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment