Index: includes/cache.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/cache.inc,v
retrieving revision 1.27
diff -u -p -r1.27 cache.inc
--- includes/cache.inc	12 Oct 2008 04:30:05 -0000	1.27
+++ includes/cache.inc	12 Jan 2009 21:43:58 -0000
@@ -15,9 +15,51 @@
  * @return The cache or FALSE on failure.
  */
 function cache_get($cid, $table = 'cache') {
+  // Garbage collection necessary when enforcing a minimum cache lifetime.
+  _cache_garbage_collection($table);
+  $cache = db_query("SELECT data, created, headers, expire, serialized FROM {" . $table . "} WHERE cid = :cid", array(':cid' => $cid))->fetchObject();
+
+  return _cache_prepare_item($cache);
+}
+
+/**
+ * Return data from the persistent cache when given an array of cache IDs.
+ *
+ * @param $cids
+ *   An array of cache IDs for the data to retrieve.
+ * @param $table
+ *   The table $table to store the data in. Valid core values are
+ *   'cache_filter', 'cache_menu', 'cache_page', or 'cache' for
+ *   the default cache.
+ * @return
+ *   An array of the items successfully returned from cache indexed by cid.
+ */
+function cache_get_multiple(array $cids, $table = 'cache') {
+  // Garbage collection necessary when enforcing a minimum cache lifetime.
+  _cache_garbage_collection($table);
+  $query = db_select($table);
+  $query->fields($table, array('cid', 'data', 'created', 'headers', 'expire', 'serialized'));
+  $query->condition($table . '.cid', $cids, 'IN');
+  $result = $query->execute();
+  $cache = array();
+  foreach ($result as $item) {
+    $item = _cache_prepare_item($item);
+    if ($item) {
+      $cache[$item->cid] = $item;
+    }
+  }
+  return $cache;
+}
+
+/**
+ * Garbage collection for cache_get() and cache_get_multiple().
+ *
+ * @param $table
+ *  The table being requested.
+ */
+function _cache_garbage_collection($table) {
   global $user;
 
-  // Garbage collection necessary when enforcing a minimum cache lifetime
   $cache_flush = variable_get('cache_flush', 0);
   if ($cache_flush && ($cache_flush + variable_get('cache_lifetime', 0) <= REQUEST_TIME)) {
     // Reset the variable immediately to prevent a meltdown in heavy load situations.
@@ -28,8 +70,19 @@ function cache_get($cid, $table = 'cache
       ->condition('expire', $cache_flush, '<=')
       ->execute();
   }
+}
 
-  $cache = db_query("SELECT data, created, headers, expire, serialized FROM {" . $table . "} WHERE cid = :cid", array(':cid' => $cid))->fetchObject();
+
+/**
+ * Utility function for preparing a cached item.
+ *
+ * @param $cache
+ *   An item loaded from cache_get() or cache_get_multiple().
+ *
+ * @return
+ *   The item with data unserialized as appropriate.
+ */
+function _cache_prepare_item($cache) {
   if (isset($cache->data)) {
     // If the data is permanent or we're not enforcing a minimum cache lifetime
     // always return the cached data.
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.845
diff -u -p -r1.845 common.inc
--- includes/common.inc	11 Jan 2009 08:39:07 -0000	1.845
+++ includes/common.inc	12 Jan 2009 21:44:01 -0000
@@ -4035,7 +4035,7 @@ function drupal_flush_all_caches() {
   node_types_rebuild();
   // Don't clear cache_form - in-progress form submissions may break.
   // Ordered so clearing the page cache will always be the last action.
-  $core = array('cache', 'cache_block', 'cache_filter', 'cache_registry', 'cache_page');
+  $core = array('cache', 'cache_block', 'cache_filter', 'cache_node', 'cache_registry', 'cache_page');
   $cache_tables = array_merge(module_invoke_all('flush_caches'), $core);
   foreach ($cache_tables as $table) {
     cache_clear_all('*', $table, TRUE);
Index: modules/book/book.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.module,v
retrieving revision 1.483
diff -u -p -r1.483 book.module
--- modules/book/book.module	29 Dec 2008 16:03:56 -0000	1.483
+++ modules/book/book.module	12 Jan 2009 21:44:01 -0000
@@ -537,6 +537,8 @@ function _book_update_outline(&$node) {
         book_update_bid($node->book);
       }
     }
+    // Clear the node_load() cache since this change may affect many nodes.
+    cache_clear_all('*', 'cache_node', TRUE);
 
     return TRUE;
   }
@@ -565,6 +567,8 @@ function book_update_bid($book_link) {
       ->condition('mlid', $mlids, 'IN')
       ->execute();
   }
+  // Clear the node_load() cache since this change may affect many nodes.
+  cache_clear_all('*', 'cache_node', TRUE);
 }
 
 /**
@@ -821,6 +825,8 @@ function book_nodeapi_delete($node) {
     db_delete('book')
       ->condition('mlid', $node->book['mlid'])
       ->execute();
+    // Clear the node_load() cache since this may affect many nodes.
+    cache_clear_all('*', 'cache_node', TRUE);
   }
 }
 
Index: modules/book/book.pages.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.pages.inc,v
retrieving revision 1.13
diff -u -p -r1.13 book.pages.inc
--- modules/book/book.pages.inc	20 Dec 2008 18:24:35 -0000	1.13
+++ modules/book/book.pages.inc	12 Jan 2009 21:44:02 -0000
@@ -214,6 +214,8 @@ function book_remove_form_submit($form, 
       ->condition('nid', $node->nid)
       ->execute();
     drupal_set_message(t('The post has been removed from the book.'));
+    // Clear the node_load() cache since this change may affect many nodes.
+    cache_clear_all('*', 'cache_node', TRUE);
   }
   $form_state['redirect'] = 'node/' . $node->nid;
 }
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.681
diff -u -p -r1.681 comment.module
--- modules/comment/comment.module	8 Jan 2009 08:42:12 -0000	1.681
+++ modules/comment/comment.module	12 Jan 2009 21:44:04 -0000
@@ -874,6 +874,7 @@ function comment_save($edit) {
       _comment_update_node_statistics($edit['nid']);
       // Clear the cache so an anonymous user can see his comment being added.
       cache_clear_all();
+      cache_clear_all($edit['nid'], 'cache_node');
 
       // Explain the approval queue if necessary, and then
       // redirect the user to the node he's commenting on.
@@ -2001,6 +2002,8 @@ function _comment_update_node_statistics
       ->condition('nid', $nid)
       ->execute();
   }
+  // Clear the node cache.
+  cache_clear_all($nid, 'cache_node');
 }
 
 /**
Index: modules/node/node.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.api.php,v
retrieving revision 1.7
diff -u -p -r1.7 node.api.php
--- modules/node/node.api.php	4 Jan 2009 16:19:39 -0000	1.7
+++ modules/node/node.api.php	12 Jan 2009 21:44:04 -0000
@@ -158,10 +158,10 @@ function hook_node_operations() {
  * Fiter, substitute or otherwise alter the $node's raw text.
  *
  * The $node->content array has been rendered, so the node body or
- * teaser is filtered and now contains HTML. This hook should only be 
- * used when text substitution, filtering, or other raw text operations 
+ * teaser is filtered and now contains HTML. This hook should only be
+ * used when text substitution, filtering, or other raw text operations
  * are necessary.
- * 
+ *
  * @param $node
  *   The node the action is being performed on.
  * @param $teaser
@@ -228,7 +228,7 @@ function hook_nodeapi_insert($node) {
  * parameter. If your module keeps track of the node types it supports, this
  * allows for an early return if nothing needs to be done.
  *
- * Due to the internal cache in node_load_multiple(), you should not use this
+ * Due to the persistent cache in node_load_multiple(), you should not use this
  * hook to modify information returned from the {node} table itself, since
  * this may affect the way nodes are returned from the cache in subsequent
  * calls to the function.
@@ -250,6 +250,43 @@ function hook_nodeapi_load($nodes, $type
 }
 
 /**
+ * Act on nodes after they have been loaded from the database.
+ *
+ * This hook should be used for altering or adding information to the node
+ * object which can not be persistently cached. For example depending on the
+ * current user or language. The results of this hook are cached during the
+ * course of a page request, so you should not alter fields returned from the
+ * {node} table here.
+ *
+ * @see poll_nodeapi_load_alter()
+ */
+function hook_nodeapi_load_alter($nodes) {
+    global $user;
+
+  foreach ($nodes as $node) {
+    if ($node->type == 'poll') {
+      // Determine whether or not this user is allowed to vote.
+      $node->allowvotes = FALSE;
+      if (user_access('vote on polls') && $node->active) {
+        if ($user->uid) {
+          $result = db_query('SELECT chid FROM {poll_vote} WHERE nid = :nid AND uid = :uid', array(':nid' => $node->nid, ':uid' => $user->uid))->fetch();
+        }
+        else {
+          $result = db_query("SELECT chid FROM {poll_vote} WHERE nid = :nid AND hostname = :hostname", array(':nid' => $node->nid, ':hostname' => ip_address()))->fetch();
+        }
+        if (isset($result->chid)) {
+          $node->vote = $result->chid;
+        }
+        else {
+          $node->vote = -1;
+          $node->allowvotes = TRUE;
+        }
+      }
+    }
+  }
+}
+
+/**
  * The node is about to be shown on the add/edit form.
  *
  * @param $node
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.1010
diff -u -p -r1.1010 node.module
--- modules/node/node.module	8 Jan 2009 08:42:12 -0000	1.1010
+++ modules/node/node.module	12 Jan 2009 21:44:05 -0000
@@ -152,6 +152,28 @@ function node_cron() {
 }
 
 /**
+ * Implementation of hook_modules_enabled().
+ */
+function node_modules_enabled($modules) {
+  foreach ($modules as $module) {
+    if (drupal_function_exists($module . '_load') || drupal_function_exists($module . '_nodeapi_load')) {
+      cache_clear_all('*', 'cache_node', TRUE);
+    }
+  }
+}
+
+/**
+ * Implementation of hook_modules_disabed().
+ */
+function node_modules_disabled($modules) {
+  foreach ($modules as $module) {
+    if (drupal_function_exists($module . '_load') || drupal_function_exists($module . '_nodeapi_load')) {
+      cache_clear_all('*', 'cache_node', TRUE);
+    }
+  }
+}
+
+/**
  * Gather a listing of links to nodes.
  *
  * @param $result
@@ -572,6 +594,7 @@ function node_type_delete($type) {
  */
 function node_type_update_nodes($old_type, $type) {
   db_query("UPDATE {node} SET type = '%s' WHERE type = '%s'", $type, $old_type);
+  cache_clear_all('*', 'cache_node', TRUE);
   return db_affected_rows();
 }
 
@@ -782,20 +805,21 @@ function node_load_multiple($nids = arra
     }
   }
 
-  // Exclude any nodes loaded from cache if they don't match $conditions.
-  // This ensures the same behavior whether loading from memory or database.
-  if ($conditions) {
-    foreach ($nodes as $node) {
-      $node_values = (array) $node;
-      if (array_diff_assoc($conditions, $node_values)) {
-        unset($nodes[$node->nid]);
-      }
+  // Load any available nodes from the persistent cache.
+  $cached_nodes = array();
+  if ($nids && !$vid && !$conditions) {
+    $cache = cache_get_multiple($nids, 'cache_node');
+    foreach ($cache as $item) {
+      $cached_nodes[$item->cid] = $item->data;
     }
+    // Remove any nodes loaded from the $nids to load.
+    $nids = array_keys(array_diff_key(array_flip($nids), $cached_nodes));
   }
 
   // Load any remaining nodes from the database. This is the case if there are
   // any $nids left to load, if loading a revision, or if $conditions was
   // passed without $nids.
+  $queried_nodes = array();
   if ($nids || $vid || ($conditions && !$passed_nids)) {
     $query = db_select('node', 'n');
 
@@ -865,25 +889,55 @@ function node_load_multiple($nids = arra
       $function = $module . '_nodeapi_load';
       $function($queried_nodes, array_keys($typed_nodes));
     }
-    $nodes += $queried_nodes;
-    // Add nodes to the cache if we're not loading a revision.
+
+    // Cache published nodes.
     if (!$vid) {
-      $node_cache += $queried_nodes;
+      foreach($queried_nodes as $node) {
+        if ($node->status) {
+          cache_set($node->nid, $node, 'cache_node');
+        }
+      }
     }
   }
 
-  // Ensure that the returned array is ordered the same as the original $nids
-  // array if this was passed in and remove any invalid nids.
-  if ($passed_nids) {
-    // Remove any invalid nids from the array.
-    $passed_nids = array_intersect_key($passed_nids, $nodes);
-    foreach ($nodes as $node) {
-      $passed_nids[$node->nid] = $node;
+  // Pass all nodes loaded from the persistent cache or the database through
+  // hook_nodeapi_load_alter() to allow modules to add information which
+  // cannot be persistently cached.
+  if (!empty($queried_nodes) || !empty($cached_nodes)) {
+    $nodes_to_process = $queried_nodes += $cached_nodes;
+    drupal_alter('nodeapi_load', $nodes_to_process);
+    $nodes = $nodes_to_process + $nodes;
+    // Statically cache the node objects at this point to avoid calling the
+    // hook multiple times in one request.
+    if (!$vid) {
+      $node_cache = $nodes_to_process + $node_cache;
+    }
+  }
+
+  // Ensure that the final array of node objects contains the same data in
+  // the same order whether loaded from cache or the node tables.
+  // $passed_nids is keyed in the same order as the original $nids array.
+  // Also remove any node IDs which weren't loaded.
+  $returned_nodes = $passed_nids ? array_intersect_key($passed_nids, $nodes) : array();
+
+  foreach ($nodes as $node) {
+    // Only return nodes which match $conditions.
+    if ($conditions) {
+      $node_values = (array) $node;
+      if (array_diff_assoc($conditions, $node_values)) {
+        unset($returned_nodes[$node->nid]);
+      }
+      else {
+        $returned_nodes[$node->nid] = $node;
+      }
+    }
+    // If no conditions, simply assign the node to the array.
+    else {
+      $returned_nodes[$node->nid] = $node;
     }
-    $nodes = $passed_nids;
   }
 
-  return $nodes;
+  return $returned_nodes;
 }
 
 /**
@@ -1072,6 +1126,7 @@ function node_save(&$node) {
 
   // Clear the page and block caches.
   cache_clear_all();
+  cache_clear_all($node->nid, 'cache_node');
 }
 
 /**
@@ -1120,6 +1175,7 @@ function node_delete($nid) {
     watchdog('content', '@type: deleted %title.', array('@type' => $node->type, '%title' => $node->title));
     drupal_set_message(t('@type %title has been deleted.', array('@type' => node_get_types('name', $node), '%title' => $node->title)));
   }
+  cache_clear_all($node->nid, 'cache_node');
 }
 
 /**
Index: modules/poll/poll.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v
retrieving revision 1.284
diff -u -p -r1.284 poll.module
--- modules/poll/poll.module	8 Jan 2009 08:42:12 -0000	1.284
+++ modules/poll/poll.module	12 Jan 2009 21:44:05 -0000
@@ -433,30 +433,12 @@ function poll_validate($node, $form) {
  * Implementation of hook_load().
  */
 function poll_load($nodes) {
-  global $user;
   foreach ($nodes as $node) {
     $poll = db_query("SELECT runtime, active FROM {poll} WHERE nid = :nid", array(':nid' => $node->nid))->fetch();
 
     // Load the appropriate choices into the $poll object.
     $poll->choice = db_query("SELECT chid, chtext, chvotes, weight FROM {poll_choice} WHERE nid = :nid ORDER BY weight", array(':nid' => $node->nid))->fetchAllAssoc('chid', PDO::FETCH_ASSOC);
 
-    // Determine whether or not this user is allowed to vote.
-    $poll->allowvotes = FALSE;
-    if (user_access('vote on polls') && $poll->active) {
-      if ($user->uid) {
-        $result = db_query('SELECT chid FROM {poll_vote} WHERE nid = :nid AND uid = :uid', array(':nid' => $node->nid, ':uid' => $user->uid))->fetch();
-      }
-      else {
-        $result = db_query("SELECT chid FROM {poll_vote} WHERE nid = :nid AND hostname = :hostname", array(':nid' => $node->nid, ':hostname' => ip_address()))->fetch();
-      }
-      if ($result) {
-        $poll->vote = $result->chid;
-      }
-      else {
-        $poll->vote = -1;
-        $poll->allowvotes = TRUE;
-      }
-    }
     foreach ($poll as $key => $value) {
       $nodes[$node->nid]->$key = $value;
     }
@@ -464,6 +446,38 @@ function poll_load($nodes) {
 }
 
 /**
+ * Implementation of hook_nodeapi_load_alter().
+ *
+ * Load user specific information into the node object for a single page
+ * request.
+ */
+function poll_nodeapi_load_alter($nodes) {
+  global $user;
+
+  foreach ($nodes as $node) {
+    if ($node->type == 'poll') {
+      // Determine whether or not this user is allowed to vote.
+      $node->allowvotes = FALSE;
+      if (user_access('vote on polls') && $node->active) {
+        if ($user->uid) {
+          $result = db_query('SELECT chid FROM {poll_vote} WHERE nid = :nid AND uid = :uid', array(':nid' => $node->nid, ':uid' => $user->uid))->fetch();
+        }
+        else {
+          $result = db_query("SELECT chid FROM {poll_vote} WHERE nid = :nid AND hostname = :hostname", array(':nid' => $node->nid, ':hostname' => ip_address()))->fetch();
+        }
+        if (isset($result->chid)) {
+          $node->vote = $result->chid;
+        }
+        else {
+          $node->vote = -1;
+          $node->allowvotes = TRUE;
+        }
+      }
+    }
+  }
+}
+
+/**
  * Implementation of hook_insert().
  */
 function poll_insert($node) {
@@ -639,6 +653,7 @@ function poll_vote($form, &$form_state) 
   db_query("UPDATE {poll_choice} SET chvotes = chvotes + 1 WHERE chid = %d", $choice);
 
   cache_clear_all();
+  cache_clear_all($node->nid, 'cache_node');
   drupal_set_message(t('Your vote was recorded.'));
 
   // Return the user to whatever page they voted from.
@@ -812,6 +827,7 @@ function poll_cancel($form, &$form_state
 
   // Subtract from the votes.
   db_query("UPDATE {poll_choice} SET chvotes = chvotes - 1 WHERE chid = %d", $node->vote);
+  cache_clear_all($node->nid, 'cache_node');
 }
 
 /**
Index: modules/system/system.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.install,v
retrieving revision 1.296
diff -u -p -r1.296 system.install
--- modules/system/system.install	9 Jan 2009 16:19:55 -0000	1.296
+++ modules/system/system.install	12 Jan 2009 21:44:07 -0000
@@ -596,6 +596,8 @@ function system_schema() {
   $schema['cache_page']['description'] = 'Cache table used to store compressed pages for anonymous users, if page caching is enabled.';
   $schema['cache_menu'] = $schema['cache'];
   $schema['cache_menu']['description'] = 'Cache table for the menu system to store router information as well as generated link trees for various menu/page/user combinations.';
+  $schema['cache_node'] = $schema['cache'];
+  $schema['cache_node']['description'] = 'Cache table for the node module to store fully loaded node objects.';
   $schema['cache_registry'] = $schema['cache'];
   $schema['cache_registry']['description'] = 'Cache table for the code registry system to remember what code files need to be loaded on any given page.';
 
@@ -3195,6 +3197,17 @@ function system_update_7017() {
 }
 
 /**
+ * Create the cache_node table.
+ */
+function system_update_7018() {
+  $ret = array();
+  $schema['cache_node'] = drupal_get_schema_unprocessed('system', 'cache');
+  $schema['cache_node']['description'] = t('Cache table used to store the output of node_load.');
+  db_create_table($ret, 'cache_node', $schema['cache_node']);
+  return $ret;
+}
+
+/**
  * @} End of "defgroup updates-6.x-to-7.x"
  * The next series of updates should start at 8000.
  */
Index: modules/taxonomy/taxonomy.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v
retrieving revision 1.451
diff -u -p -r1.451 taxonomy.module
--- modules/taxonomy/taxonomy.module	30 Dec 2008 16:43:19 -0000	1.451
+++ modules/taxonomy/taxonomy.module	12 Jan 2009 21:44:08 -0000
@@ -321,8 +321,10 @@ function taxonomy_term_save($term) {
   else {
     $status = drupal_write_record('term_data', $term);
     module_invoke_all('taxonomy_term_update', $term);
+    // This change can affect a large number of nodes, so clear the node cache.
+    cache_clear_all('*', 'cache_node', TRUE);
   }
-  
+
   $or = db_or()->condition('tid1', $term->tid)->condition('tid2', $term->tid);
   db_delete('term_relation')->condition($or)->execute();
 
@@ -407,6 +409,8 @@ function taxonomy_term_delete($tid) {
   }
 
   cache_clear_all();
+  // This change can affect a lot of nodes, so clear the node cache.
+  cache_clear_all('*', 'cache_node', TRUE);
 
   return SAVED_DELETED;
 }
Index: modules/translation/translation.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/translation/translation.module,v
retrieving revision 1.37
diff -u -p -r1.37 translation.module
--- modules/translation/translation.module	16 Dec 2008 22:05:51 -0000	1.37
+++ modules/translation/translation.module	12 Jan 2009 21:44:09 -0000
@@ -223,6 +223,7 @@ function translation_nodeapi_insert($nod
         db_query("UPDATE {node} SET tnid = %d, translate = %d WHERE nid = %d", $tnid, 0, $node->translation_source->nid);
       }
       db_query("UPDATE {node} SET tnid = %d, translate = %d WHERE nid = %d", $tnid, 0, $node->nid);
+      translation_clear_node_cache($tnid);
     }
   }
 }
@@ -240,6 +241,7 @@ function translation_nodeapi_update($nod
         // This is the source node, asking to mark all translations outdated.
         db_query("UPDATE {node} SET translate = 1 WHERE tnid = %d AND nid <> %d", $node->tnid, $node->nid);
       }
+      translation_clear_node_cache($node->tnid);
     }
   }
 }
@@ -274,6 +276,22 @@ function translation_remove_from_set($no
         db_query('UPDATE {node} SET tnid = %d WHERE tnid = %d', $new_tnid, $node->tnid);
       }
     }
+    translation_clear_node_cache($node->tnid);
+  }
+}
+
+/**
+ * Clear the node_load() cache for all nodes in the translation set.
+ *
+ * This ensures we have the proper translation set information in every node.
+ *
+ * @param $tnid
+ *   The translation source nid of the translation set, the identifier
+ *   of the node used to derive all translations in the set.
+ */
+function translation_clear_node_cache($tnid) {
+  foreach (translation_node_get_translations($tnid) as $node) {
+    cache_clear_all($node->nid, 'cache_node');
   }
 }
 
