Index: menu.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/menu.inc,v
retrieving revision 1.255.2.31
diff -u -r1.255.2.31 menu.inc
--- menu.inc	27 Apr 2009 12:50:13 -0000	1.255.2.31
+++ menu.inc	15 Jul 2009 00:37:51 -0000
@@ -1682,31 +1682,69 @@
 
 /**
  * Collect, alter and store the menu definitions.
+ * 
+ * This (modified) function should remain in menu.inc,
+ * so it can be called by other modules without require_once.
+ * 
+ * @param $reset
+ * @param $changes object to collect $insert, $update, $delete arrays
+ * @return the menu array
  */
-function menu_router_build($reset = FALSE) {
+function menu_router_build($reset = FALSE, $changes = NULL) {
+  require_once 'includes/menu.rebuild_router.inc';
   static $menu;
 
   if (!isset($menu) || $reset) {
-    // We need to manually call each module so that we can know which module
-    // a given item came from.
-    $callbacks = array();
-    foreach (module_implements('menu') as $module) {
-      $router_items = call_user_func($module .'_menu');
-      if (isset($router_items) && is_array($router_items)) {
-        foreach (array_keys($router_items) as $path) {
-          $router_items[$path]['module'] = $module;
-        }
-        $callbacks = array_merge($callbacks, $router_items);
-      }
-    }
-    // Alter the menu as defined in modules, keys are like user/%user.
-    drupal_alter('menu', $callbacks);
-    $menu = _menu_router_build($callbacks);
+    $menu = _menu_router_build_rebuild($changes);
     _menu_router_cache($menu);
   }
   return $menu;
 }
 
+
+/**
+ * To be called by menu_router_build()
+ * 
+ * TODO: Move in separate file (menu.rebuild_router.inc)
+ * Right now it's in menu.inc, to make the patch easier to read.
+ * 
+ * @param $changes object to collect $insert, $update, $delete arrays
+ */
+function _menu_router_build_rebuild($changes = NULL) {
+  
+  $callbacks = _menu_router_build_calc_callbacks();
+  // echo '<br/>NEW callbacks b) ' . implode(', ', array_keys(end($callbacks)));
+  list($menu, $masks) = _menu_router_build_calc_menu($callbacks);
+  
+  if (!$menu) {
+    // We must have a serious error - there is no data to save.
+    watchdog('php', 'Menu router rebuild failed - some paths may not work correctly.', array(), WATCHDOG_ERROR);
+    return array();
+  }
+  
+  // calculate new table contents
+  $rows_new = _menu_router_build_calc_rows($menu);
+  
+  // compare with old table contents
+  if (!is_object($changes)) {
+    $changes = new stdClass;
+  }
+  $rows_old = _menu_router_build_load_rows();
+  _menu_router_build_calc_changes($rows_old, $rows_new, $changes);
+  
+  // echo '<pre>'; print_r($changes); echo '</pre>';
+  
+  // save changes
+  _menu_router_build_save_insert($changes->insert);
+  _menu_router_build_save_update($changes->update);
+  _menu_router_build_save_delete($changes->delete);
+  
+  // save the masks
+  variable_set('menu_masks', $masks);
+  
+  return $menu;
+}
+
 /**
  * Helper function to store the menu router if we have it in memory.
  */
@@ -2232,9 +2270,22 @@
 }
 
 /**
- * Helper function to build the router table based on the data from hook_menu.
+ * Calculate the menu from given callbacks,
+ * but don't touch the DB.
+ * 
+ * Implementation based on the old function
+ * _menu_router_build($callbacks)
+ * 
+ * TODO:
+ *   - Move into a separate file (menu.rebuild_router.inc)
+ *   - Split up into smaller functions?
+ *   For now this is not done to make the patch easier to compare.
+ *   (hopefully it is synced with the old _menu_router_build)
+ * 
+ * @param $callbacks array collected from hook_menu implementations
+ * @return array($menu, $masks)
  */
-function _menu_router_build($callbacks) {
+function _menu_router_build_calc_menu($callbacks) {
   // First pass: separate callbacks from paths, making paths ready for
   // matching. Calculate fitness, and fill some default values.
   $menu = array();
@@ -2317,13 +2368,6 @@
   }
   array_multisort($sort, SORT_NUMERIC, $menu);
 
-  if (!$menu) {
-    // We must have a serious error - there is no data to save.
-    watchdog('php', 'Menu router rebuild failed - some paths may not work correctly.', array(), WATCHDOG_ERROR);
-    return array();
-  }
-  // Delete the existing router since we have some data to replace it.
-  db_query('DELETE FROM {menu_router}');
   // Apply inheritance rules.
   foreach ($menu as $path => $v) {
     $item = &$menu[$path];
@@ -2403,31 +2447,13 @@
       $file_path = $item['file path'] ? $item['file path'] : drupal_get_path('module', $item['module']);
       $item['include file'] = $file_path .'/'. $item['file'];
     }
-
-    $title_arguments = $item['title arguments'] ? serialize($item['title arguments']) : '';
-    db_query("INSERT INTO {menu_router}
-      (path, load_functions, to_arg_functions, access_callback,
-      access_arguments, page_callback, page_arguments, fit,
-      number_parts, tab_parent, tab_root,
-      title, title_callback, title_arguments,
-      type, block_callback, description, position, weight, file)
-      VALUES ('%s', '%s', '%s', '%s',
-      '%s', '%s', '%s', %d,
-      %d, '%s', '%s',
-      '%s', '%s', '%s',
-      %d, '%s', '%s', '%s', %d, '%s')",
-      $path, $item['load_functions'], $item['to_arg_functions'], $item['access callback'],
-      serialize($item['access arguments']), $item['page callback'], serialize($item['page arguments']), $item['_fit'],
-      $item['_number_parts'], $item['tab_parent'], $item['tab_root'],
-      $item['title'], $item['title callback'], $title_arguments,
-      $item['type'], $item['block callback'], $item['description'], $item['position'], $item['weight'], $item['include file']);
   }
-  // Sort the masks so they are in order of descending fit, and store them.
+  
+  // Sort the masks so they are in order of descending fit.
   $masks = array_keys($masks);
   rsort($masks);
-  variable_set('menu_masks', $masks);
-
-  return $menu;
+  
+  return array($menu, $masks);
 }
 
 /**
--- menu.rebuild_router.inc
+++ menu.rebuild_router.inc
@@ -0,0 +1,184 @@
+<?php
+
+
+
+
+/**
+ * Calculate the new content for the menu_router table
+ * 
+ * @param $menu array as generated by _menu_router_build_calc_menu()
+ * @return array the rows, as in db_fetch_array().
+ */
+function _menu_router_build_calc_rows(array $menu) {
+  foreach ($menu as $path => $v) {
+    $item = &$menu[$path];
+    $rows[$path] = array(
+      'path' => $path,
+      'load_functions' => $item['load_functions'],
+      'to_arg_functions' => $item['to_arg_functions'],
+      'access_callback' => $item['access callback'],
+      'access_arguments' => serialize($item['access arguments']),
+      'page_callback' => $item['page callback'],
+      'page_arguments' => serialize($item['page arguments']),
+      'fit' => $item['_fit'],
+      'number_parts' => $item['_number_parts'],
+      'tab_parent' => $item['tab_parent'],
+      'tab_root' => $item['tab_root'],
+      'title' => $item['title'],
+      'title_callback' => $item['title callback'],
+      'title_arguments' => $item['title arguments'] ? serialize($item['title arguments']) : '',
+      'type' => $item['type'],
+      'block_callback' => $item['block callback'],
+      'description' => $item['description'],
+      'position' => $item['position'],
+      'weight' => $item['weight'],
+      'file' => $item['include file'],
+    );
+  }
+
+  return $rows;
+}
+
+
+/**
+ * Get callbacks from hook_menu implementations
+ * 
+ * @return array callbacks
+ */
+function _menu_router_build_calc_callbacks() {
+  // We need to manually call each module so that we can know which module
+  // a given item came from.
+  $callbacks = array();
+  foreach (module_implements('menu') as $module) {
+    $router_items = call_user_func($module .'_menu');
+    if (isset($router_items) && is_array($router_items)) {
+      foreach (array_keys($router_items) as $path) {
+        $router_items[$path]['module'] = $module;
+      }
+      $callbacks = array_merge($callbacks, $router_items);
+    }
+  }
+  // Alter the menu as defined in modules, keys are like user/%user.
+  drupal_alter('menu', $callbacks);
+  return $callbacks;
+}
+
+
+/**
+ * Find the changes to be stored in the menu_router table
+ * 
+ * @param $rows_old
+ * @param $rows_new
+ * @param $changes object to collect $insert, $update, $delete arrays
+ * @return $changes object
+ */
+function _menu_router_build_calc_changes($rows_old, $rows_new, $changes = NULL) {
+  if (!is_object($changes)) {
+    $changes = new stdClass;
+  }
+  $changes->insert = array();
+  $changes->update = array();
+  $changes->delete = array();
+  foreach ($rows_new as $path => $row_new) {
+    // echo '<br/>banana: ' . $path;
+    if (!isset($rows_old[$path])) {
+      // echo ' - insert';
+      $changes->insert[$path] = $row_new;
+    } else {
+      // echo ' - compare';
+      $row_old = $rows_old[$path];
+      $row_changes = array();
+      foreach ($row_new as $key => $value) {
+        if ($value != $row_old[$key]) {
+          $row_changes[$key] = $value;
+        }
+      }
+      if (!empty($row_changes)) {
+        // echo ' - update';
+        $changes->update[$path] = $row_changes;
+      }
+      unset($rows_old[$path]);
+    }
+  }
+  // delete remaining rows
+  foreach ($rows_old as $path => $row_old) {
+    $changes->delete[$path] = $path;
+  }
+  // echo '<pre>'; print_r($changes); echo '</pre>';
+  return $changes;
+}
+
+
+/**
+ * Load the old contents of menu_router
+ * 
+ * @return array rows as in db_fetch_array, keyed by $path
+ */
+function _menu_router_build_load_rows() {
+  $rows = array();
+  $result = db_query("SELECT * FROM {menu_router}");
+  while ($row = db_fetch_array($result)) {
+    $rows[$row['path']] = $row; 
+  }
+  return $rows;
+}
+
+/**
+ * Execute INSERT queries
+ * 
+ * @param $insert array of rows to insert
+ */
+function _menu_router_build_save_insert($insert) {
+  // TODO: ON DUPLICATE KEY UPDATE ?
+  if (count($insert)) {
+    // the $sql is always the same
+    $fields = array();
+    $tokens = array();
+    $example_row = end($insert);
+    foreach ($example_row as $k => $v) {
+      $fields[] = $k;
+      $tokens[] = "'%s'";
+    }
+    $fields = implode(', ', $fields);
+    $tokens = implode(', ', $tokens);
+    $sql = "INSERT INTO {menu_router} ($fields) VALUES ($tokens)";
+    // insert the rows
+    foreach ($insert as $row) {
+      $args = array_values($row);
+      array_unshift($args, $sql);
+      call_user_func_array('db_query', $args);
+    }
+  }
+}
+
+/**
+ * Execute UPDATE queries
+ * 
+ * @param $update array of rows with modifications
+ */
+function _menu_router_build_save_update($update) {
+  foreach ($update as $changes) {
+    $set = array();
+    foreach ($changes as $k => $v) {
+      $set[] = "$k = %s";
+    }
+    $set = implode(', ', $set);
+    $sql = "UPDATE {menu_router} SET $set";
+    $args = array_values($changes);
+    array_unshift($args, $sql);
+    call_user_func_array('db_query', $args);
+  }
+}
+
+/**
+ * Execute DELETE queries
+ * 
+ * @param $delete array of paths (PRIMARY KEY) to delete
+ */
+function _menu_router_build_save_delete($delete) {
+  $sql = "DELETE FROM {menu_router} WHERE path = '%s'";
+  foreach ($delete as $path) {
+    db_query($sql, $path);
+  }
+}
+


