diff --git modules/simpletest/simpletest.info modules/simpletest/simpletest.info index 63f61e6..076c606 100644 --- modules/simpletest/simpletest.info +++ modules/simpletest/simpletest.info @@ -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 diff --git modules/simpletest/tests/upgrade/upgrade.taxonomy.test modules/simpletest/tests/upgrade/upgrade.taxonomy.test new file mode 100644 index 0000000..5c3eec2 --- /dev/null +++ modules/simpletest/tests/upgrade/upgrade.taxonomy.test @@ -0,0 +1,122 @@ + '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(); + } + + /** + * 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.')); + + $vocabularies = taxonomy_get_vocabularies(); + + // Check that the node type 'page' has been associated to a taxonomy + // reference field for each vocabulary. + foreach (field_info_instances('node', 'page') 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']; + } + } + } + } + $this->assertEqual(sort(array_keys($vocabularies)), sort(array_keys($instances)), t('Node type page has instances for every vocabulary.')); + + // Check that the node type 'story' has been associated to a taxonomy + // reference field for each vocabulary. It was not explicitely in + // $vocabulary->nodes but each node of type 'story' was associated to + // one or more terms. + $instances = array(); + foreach (field_info_instances('node', 'story') as $instance) { + $field = field_info_field($instance['field_name']); + if ($field['type'] == 'taxonomy_term_reference') { + foreach ($field['settings']['allowed_values'] as $tree) { + if (empty($instances[$tree['vid']]) || $instances[$tree['vid']] == 'taxonomyextra') { + $instances[$tree['vid']] = $field['field_name']; + } + } + } + } + $field_names = array_flip($instances); + $this->assertEqual(count($field_names), 1, t('Only one taxonomy term field is used for on story nodes')); + $this->assertEqual(key($field_names), 'taxonomyextra', t('Only the excess taxonomy term field is used for on story nodes')); + + // Check that the node type 'poll' has been associated to no taxonomy + // reference field. + $instances = array(); + foreach (field_info_instances('node', 'poll') as $instance) { + $field = field_info_field($instance['field_name']); + if ($field['type'] == 'taxonomy_term_reference') { + foreach ($field['settings']['allowed_values'] as $tree) { + if (empty($instances[$tree['vid']]) || $instances[$tree['vid']] == 'taxonomyextra') { + $instances[$tree['vid']] = $field['field_name']; + } + } + } + } + $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'); + $this->drupalSetContent(drupal_render($node->content)); + 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)); + } + } + } + } +} diff --git modules/taxonomy/taxonomy.install modules/taxonomy/taxonomy.install index 680403d..a95f5ca 100644 --- modules/taxonomy/taxonomy.install +++ modules/taxonomy/taxonomy.install @@ -412,7 +412,54 @@ 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. + + // Allowed values for this extra vocabs field is every vocabulary. + 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); @@ -423,14 +470,6 @@ function taxonomy_update_7004() { * Migrate {taxonomy_term_node} table to field storage. */ 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); // This is a multi-pass update. On the first call we need to initialize some // variables. @@ -440,47 +479,144 @@ 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; + } + } + + if (!empty($vocabularies)) { + $sandbox['vocabularies'] = $vocabularies; } + } + else { + $field_info = field_info_fields(); + $etid = _field_sql_storage_etid('node'); - // 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(); + // 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. + $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 LEFT JOIN {node} n ON tn.nid = n.nid LEFT JOIN {node} n2 ON tn.vid = n2.vid ORDER BY tn.vid, td.weight ASC'; + $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 for a single value field, 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); + } + } } }