Note: for migrate menu destination support and a Drupal-to-Drupal menu migration class that uses it, see #763880: Import Hierarchy as Book and #2146961: Preserve Book hierarchies respectively.

Migrating book module nodes is one of the more difficult tasks to accomplish with Migrate module. This is due largely to certain workflow issues that can come up during migrating book nodes, and the fact that book node hierarchy is stored within menu links (Drupal 6 and later). The following code is an example of how to deal with this.

For migrating from Drupal 5 see below. The concept used there may also be used for migrating from Drupal 6 with some rewrites (to join the menu links). Its advantage is, that only one migration is used instead of two.

Migrating from Drupal 6

Overview

  • Migrate nodes as normal;
  • Get hierarchy from the source database 'book' table, and weights from the 'menu_links' table;
  • Use complete() in your Migration class to call _book_update_outline() to migrate the hierarchy.

Explanation

Books are nodes, so they can inherit most of their migration from a common class. You can use something like the Drupal-to-Drupal data migration classes, or roll your own, like the one provided below.

Node migration

Let's kick this off with a good NodeMigration abstract class that should get us most of the way there. This class should be useful for other node types as well.

abstract class NodeMigration extends Migration {
  public function __construct(array $arguments) {
    parent::__construct($arguments);
    $type = isset($arguments['type']) ? $arguments['type'] : NULL;
    $new_type = isset($arguments['new type']) ? $arguments['new type'] : $type;
    $this->description = t('Migrate nodes');
    $this->map = new MigrateSQLMap($this->machineName,
      array(
        'nid' => array(
          'type' => 'int',
          'unsigned' => TRUE,
          'not null' => TRUE,
          'description' => 'd6 Unique Node ID',
          'alias' => 'n',
        )
      ),
      MigrateDestinationNode::getKeySchema()
    );

    $query = $this->node_query($type);
    $this->set_highwater_field();
    $this->set_source($query);
    $this->destination = new MigrateDestinationNode($new_type);
    $this->node_field_mapping();
  }

  public function set_highwater_field() {
    $this->highwaterField = array(
      'name' => 'changed',
      'alias' => 'n',
    );
  }

  public function node_query($type) {
    $query = Database::getConnection('d6')
      ->select('node', 'n')
      ->fields('n', array('nid', 'vid', 'title', 'uid', 'status', 'created', 'changed', 'comment', 'promote', 'moderate', 'sticky'))
      ->condition('n.type', $type);
    $query->join('node_revisions', 'nr', 'nr.vid = n.vid');
    $query->fields('nr', array('body', 'teaser', 'format'));
    return $query;
  }

  public function set_source($query) {
    $this->source = new MigrateSourceSQL($query);
    $this->source->setMapJoinable(FALSE);
  }

  public function node_field_mapping() {
    $body_arguments = MigrateTextFieldHandler::arguments(array('source_field' => 'teaser'), array('source_field' => 'format'), NULL);
    // Make the mappings
    $this->addSimpleMappings(
      array(
        // Assumes you have done a migration of Users which maintains uids.
        'uid',
        'title',
        'status',
        'created',
        'changed',
        'comment',
        'promote',
        'moderate',
        'sticky',
      )
    );
    $this->addFieldMapping('body', 'body')->arguments($body_arguments);
    $this->addFieldMapping('language')->defaultValue('en');
    $this->addFieldMapping('is_new')->defaultValue(TRUE);
  }

  public function prepareRow($current_row) {
    $formats = array(
      '1' => 'filtered_html',
      '2' => 'full_html',
      '3' => 'plain_text',
      '4' => 'markdown',
    );
    $current_row->format = isset($formats[$current_row->format]) ? $formats[$current_row->format] : 'plain_text';
  }
}

There are some components of this you should change for your site, especially the $formats array in prepareRow(). In general this breaks a typical node migration into its various components of:

  1. What are we migrating? (mostly $this->map)
  2. How do we get it? ($this->node_query())
  3. How do we update it later? ($this->set_highwater_field())
  4. Set the source
  5. Set the Destination
  6. Do field mappings ($this->node_field_mapping())

This gives us enough granularity to write fairly minor migration classes from this point forward. As an example, migrating the default page nodes with this in hand looks something like:

class NodePageMigration extends NodeMigration {
  public function __construct() {
    parent::__construct(array('type' => 'page'));
  }
}

And migrating Story nodes to Article nodes could look like:

class NodeArticleMigration extends NodeMigration {
  public function __construct() {
    parent::__construct(array('type' => 'story', 'new type' => 'article'));
  }
}

Adding book hierarchies

So, with that foundation, let's discuss migrating book nodes.

The only difference between book nodes and other types of nodes is their table of contents hierarchy. This data is stored in its own table, handily called 'book'. There's also some information about relative weights for book items stored in menus.

There's three things we need to do to bring in the TOC data:

  • Get the source data from the source database;
  • Map source nids to destination nids;
  • Update each migrated book with its position in its TOC.

class NodeBookMigration extends NodeMigration {
  public function __construct() {
    parent::__construct(array('type' => 'book'));
  }

  /**
   * Overrides parent::nodeQuery to add more data to the source, in our case,
   * book hierarchy stuff.
   */
  public function nodeQuery() {
    $query = parent::nodeQuery();
    // Add in book parent child relationships.
    $query->leftJoin('book', 'b', 'n.nid = b.nid');
    $query->addField('b', 'bid', 'book_id');
    $query->leftJoin('menu_links', 'ml', 'b.mlid = ml.mlid');
    $query->addField('ml', 'weight', 'book_weight');

    return $query;
  }
  
  /**
   * Acts right after a book node has been saved. Map the book hierarchy.
   *
   * @param object $node
   *   A node object for the newly migrated book.
   * @param stdClass $row
   *   An object representing data from the source row.
   */
  public function complete($node, stdClass $row) {
    // If this is a top-level book, don't set a parent.
    $book_id = $row->nid == $row->book_id ? $node->nid : $this->lookupMigratedBook($row->book_id);
    // If we're updating a migration, this node may already have a mlid.
    $mlid = db_query("SELECT mlid FROM {book} WHERE nid = :nid", array(
      ':nid' => $node->nid,
    ))->fetchAssoc();
    $mlid = (empty($mlid)) ? NULL : $mlid['mlid'];
    // Book id - effectively, parent nid.
    $node->book['bid'] = $book_id;
    $node->book['nid'] = $node->nid;
    $node->book['mlid'] = $mlid;
    $node->book['weight'] = $row->book_weight;
    _book_update_outline($node);
    node_save($node);
  }

  /**
   * Returns a mapping for a migrated book.
   *
   * @param int $source_book_nid
   *   Nid of book in source database to lookup.
   */
  protected function lookupMigratedBook($source_book_nid) {
    $dest_book_nid = parent::handleSourceMigration('NodeBook', $source_book_nid);
    return $dest_book_nid;
  }
}

So far, so good. If you check your migration at admin/content/migrate, you should be able to see the new fields in your source data.

Migrate module helps us out in two ways here.

  • We use a complete() method which runs straight after an individual node has been migrated. This way, we can just call a book module function to add book-specific data.
  • Migrate API keeps mappings of source nids to migrated nids. We can then manually call parent::handleSourceMigration() to lookup the mapping. Previously, I tried setting a sourceMigration on the book_parent_nid field mapping, but this creates unwanted stubs for top-level books. Doing a manual lookup ensures that nodes are only looked up (or created as stubs) when required.

I have used this approach to migrate several hundred book nodes with great success. I highly recommend using Drupal-to-Drupal data migration rather than rolling your own node migration classes, but every Migration is different, so do what you have to.

Migrating from Drupal 5

Drupal 5 does not make use of the menu system for books as Drupal 6. It's all in the book table.

To import books from Drupal 5 I use migrate_d2d, a custom destination handler and an extended DrupalNode5Migration class as below (all together in my node_book.inc file).

You have to check how deep is your deepest book hierarchy (in my case it is 5). Not sure if this could be done dynamically or in one central place, but for me setting $max_depth manually is just fine.

The Migration could be done in one single migration by joining all necessary information in one query. This includes multiple joins of the book table on the parent nid with the book items nid, to get the parent of a parent and its parent and so on.

The orderBy clause makes sure that all parents are imported before the children. As one book page has only one parent this should be safe and there should be no need to create stubs.

Attention: Maybe the mapping from legacy nids to new nids is missing as I use the value mapped to nid as its ID, rather than generated a new sequential ID (see MigrateDestinationNode > Fields > is_new) – so stuff in prepare() should better be in complete().

/**
 * @file
 * Implementation of MyNodeBookMigration for Drupal 5 sources.
 */

/**
 * Handling book nodes with book type article.
 */
class MyNodeBookMigration extends DrupalNode5Migration {

  /**
   * @param array $arguments
   */
  public function __construct(array $arguments) {
    parent::__construct($arguments);

    $this->addFieldMapping('book', NULL)
         ->description('Is book/bookpage')
         ->defaultValue(TRUE);

    $this->addFieldMapping('book:pid', 'b_parent')
         ->description('Book Parent')
         ->defaultValue(0);

    $this->addFieldMapping('book:weight', 'b_weight')
         ->description('Book Weight')
         ->defaultValue(0);

    $max_depth = 5;
    for ($depth = 1; $depth <= $max_depth; $depth++) {
      // $this->query().
      $this->addFieldMapping('book:nid' . $depth, 'b' . $depth . '_nid')
           ->description('Book Parent ID from join nr. ' .$depth);
    }
  }
  /**
   * Query for basic node fields from Drupal 5.
   *
   * @return QueryConditionInterface
   */
  protected function query() {
    $query = parent::query();

    $query->innerJoin('book', 'b', 'n.vid=b.vid');
    $query->addField('b', 'weight', 'b_weight');
    $query->addField('b', 'parent', 'b_parent');
    // $query->isNotNull('b.vid');

    // Join book table on parent node ids $max_depth times to sort nodes, so
    // that all book-nodes (parents = 0 come first), then all subpages and then
    // all sub-subpages so on till the pages with maximum depth.
    // In our case is 5 for maximum depth enough.
    $max_depth = 5;
    for ($depth = 1; $depth <= $max_depth; $depth++) {
      // Alias for the first time book table
      // was joined is b, not b0.
      $query->addField('b' . $depth, 'nid', 'b' . $depth . '_nid');
      $condition = sprintf('b%s.parent=b%s.nid', $depth - 1 == 0 ? '' : $depth - 1, $depth);
      $query->leftJoin('book', 'b' . $depth, $condition);
      // Sort ascending so NULL comes first, starting with the deepest item
      // ensures that all parent items where created before adding an item to
      // to the outline.
      if ($max_depth - $depth) {
        $query->orderBy('b' . ($max_depth - $depth) . '.parent', 'ASC');
      }
    }
    $query->orderBy('n.nid');

    return $query;
  }
}

class MigrateBookDestinationHandler extends MigrateDestinationHandler {
  public function __construct() {
    $this->registerTypes(array('node'));
  }

  /**
   * Implementation of MigrateDestinationHandler::fields().
   */
  public function fields($entity_type, $bundle, $migration = NULL) {
    $fields = array();

    if (module_exists('book')) {
      $fields['book'] = t('Is book/bookpage.');
      $fields['book:bid'] = t('Subfield: ID of book to add this bookpage.');
      $fields['book:pid'] = t('Subfield: Parent Node ID.');
      $fields['book:weight'] = t('Subfield: Book weight.');

      $max_depth = 5;
      for ($depth = 1; $depth <= $max_depth; $depth++) {
        $fields['book:nid' . $depth] = t('Subfield: !depth. parents node ID.', array('!deepth' => $depth));
      }
    }

    return $fields;
  }

  public function prepare($entity, stdClass $row) {
    if (module_exists('book') && !empty($entity->book[0])) {

      // Unset the migrate variable $entity->book to set book object later;
      unset($entity->book[0]);

      $arguments = array();
      if (isset($entity->book['arguments'])) {
        $arguments = $entity->book['arguments'];
        unset($entity->book['arguments']);
      }

      // bid = nid in case it book-page is book itself.
      $entity->book['bid'] = $entity->nid;

      // In case the book has parents, we need to go to the top parent to get
      // the book id. Parents should be in b{$depth}_nid, see join.
      $max_depth = 5;
      for ($depth = 1; $depth <= $max_depth; $depth++) {
        if (!empty($arguments['nid' . $depth])) {
          $entity->book['bid'] = $arguments['nid' . $depth];
        }
        else {
          // If $depth parent is empty (0 or NULL) we already have the book id.
          break;
        }
      }

      // Get menu link id of parent.
      if (empty($entity->book['plid']) && $arguments['pid'] != $entity->book['bid']) {
        $entity->book['plid'] = db_select('book', 'b')
          ->fields('b', array('mlid'))
          ->condition('nid', $arguments['pid'])
          ->execute()
          ->fetchField();
      }

      if ($arguments['weight']) {
        $entity->book['weight'] = $arguments['weight'];
      }

      if ($entity->nid) {
        // For Updates we need to set book['mlid'], otherwise we get PDOException
        // because of inerting a duplicate entry (see _book_update_outline()).
        $query = db_select('book', 'b')
                  ->fields('b', array('mlid'))
                  ->condition('nid', $entity->nid);
        $query->leftJoin('menu_links', 'ml', 'b.mlid=ml.mlid');
        $query->fields('ml', array('has_children'));
        $book = $query->execute()->fetchObject();

        if ($book->mlid) {
          $entity->book['mlid'] = $book->mlid;
        }
        if (!isset($entity->book['has_children']) && $book->has_children) {
          $entity->book['has_children'] = $book->has_children;
        }
      }
    }
  }
}

Revisions

The book migration will always crate a new revision, if it is not executed by a user with the 'administer nodes' permissions, for example when using drush without the global option--user. For more details see issue #2504813: Updating book nodes allways crates new revision

Comments

Neograph734’s picture

Great code example, but I got all nodes to be a child of the 'root', while my original book had serveral parents-child relations. Therefor I came up with the following approch:

Extend the query to also get the old parent's nid. (You could do a lookup with the 'book' table, but it is in the link_path as well (in the form "node/[nid]").)

<?php
// Original query
    $query->join('book', 'b', 'n.nid = b.nid');
    $query->addField('b', 'bid', 'book_id');
    $query->join('menu_links', 'ml', 'b.mlid = ml.mlid');
    $query->addField('ml', 'weight', 'book_weight');
//Query extention to get nid of parent
    $query->leftJoin('menu_links', 'mm', 'ml.plid = mm.mlid');
    $query->addField('mm', 'link_path', 'parent_id');
// Sort by depth so there is always a parent
    $query->orderBy('ml.depth');
?>

Then in the complete function we add another query:

<?php
public function complete($node, stdClass $row) {
    // If this is a top-level book, don't set a parent.
    $book_id = $row->nid == $row->book_id ? $node->nid : parent::handleSourceMigration('NodeBook', $row->book_id);
    // Book id - effectively, parent nid.
    $node->book['bid'] = $book_id;
    $node->book['nid'] = $node->nid;
    $node->book['weight'] = $row->book_weight;

// We strip the "node/"-part from the link_path
    $row->parent_id = str_replace("node/", "", $row->parent_id);
// If there is a parent we lookup the migrated node
    if(isset($row->parent_id) && $row->parent_id > 0) {
      $parent_node = parent::handleSourceMigration('NodeBook', $row->parent_id);
// We get the mlid of the migrated parent      
      $query = db_select("menu_links", "ml")
        ->fields("ml")
        ->condition("ml.link_path", "node/" . $parent_node, "=")
        ->execute()
        ->fetchAssoc();
      $plid = $query['mlid'];
// We add the parent mlid (plid)
      $node->book['plid'] = $plid;
    }
    _book_update_outline($node);
    node_save($node);
  }
?>

It probably isn't the best way to do this, but it works.

ajlozier’s picture

Thanks for the great article! For others wishing to use migrate_d2d, here is how we accomplished it, using the information in the article as our starting point.

Basically if you are using migrate_d2d, you can skip the entire section on "Node migration" since this is handled already by the DrupalNodeMigration class.

Rather than nodeQuery, however, DrupalNodeMigration uses a function called query(). However it does exactly the same thing as in your example. (See http://drupal.org/node/1819738 for more information).

So here was our final migration class:

class BookPageMigration extends DrupalNode6Migration {
  public function __construct(array $arguments) {
    parent::__construct($arguments);
  }
  
    /**
    * Overrides parent::query to add more data to the source, in our case,
    * book hierarchy stuff.
    */
  public function query() {
    $query = parent::query();
    // Add in book parent child relationships.
    $query->join('book', 'b', 'n.nid = b.nid');
    $query->addField('b', 'bid', 'book_id');
    $query->join('menu_links', 'ml', 'b.mlid = ml.mlid');
    $query->addField('ml', 'weight', 'book_weight');
    return $query;
  }
  
    /**
   * Acts right after a book node has been saved. Map the book hierarchy.
   *
   * @param object $node
   *   A node object for the newly migrated book.
   * @param stdClass $row
   *   An object representing data from the source row.
   */
   public function complete($node, stdClass $row) {
    // If this is a top-level book, don't set a parent.
    $book_id = $row->nid == $row->book_id ? $node->nid : $this->lookupMigratedBook($row->book_id);
    // Book id - effectively, parent nid.
    $node->book['bid'] = $book_id;
    $node->book['nid'] = $node->nid;
    $node->book['weight'] = $row->book_weight;
    _book_update_outline($node);
    node_save($node);
  }   
  
/**
   * Returns a mapping for a migrated book.
   *
   * @param int $source_book_nid
   *   Nid of book in source database to lookup.
   */
  protected function lookupMigratedBook($source_book_nid) {
    $dest_book_nid = parent::handleSourceMigration('ExBookPage', $source_book_nid);
    return $dest_book_nid;
  }
  
}

Note that "ExBookPage" was the "machine_name" we gave to this migration in our custom migration module. Worked like a charm! Hope this helps someone else.

EclipseGc’s picture

I really appreciate all the expanding on this topic that others are doing. When I wrote the original it was out of frustration of not being able to find any practical examples of how this could be done, so seeing other use it and go further is very encouraging. Keep it up!

Eclipse

Eclipse

CWSmith1701’s picture

Can this be used with a D2D setup with Drupal 7 as the source?

EclipseGc’s picture

I kind of doubt it, but I DO think D2D sets up stub nodes, so that might actually make this whole thing a lot easier and remove the insanity I did trying to sort the nodes in some order that made this easier.

Eclipse

Eclipse

bohemier’s picture

Can you use this code in conjunction with migrade_d2d_ui ? I.e. Can you plug this code to a ui driven migration? If not, how to you register your class? Thanks !

J-F Bohémier
Angelicode.com

rsbecker’s picture

This discussion was very helpful to me. But I found one problem and I don't know how to address it.

This class only finds book nodes, not other types of nodes that are book pages. Either there needs to be a stub function built into this so it can be run before migrating other content types or it needs to be able to find existing nodes from the book table and create menu links for them.

Neograph734’s picture

Eclipse has the solution in his first post. This part in particular, where you can find certain content types and map them to another.

<?php
class NodeArticleMigration extends NodeMigration {
  public function __construct() {
    parent::__construct(array('type' => 'story', 'new type' => 'article'));
  }
}
?>

If you use that in the book migration like below,you can take all non-book-books and migrate them to a book (or to another content type). I ended up with 3 copies of this class with 3 different 'type' parameters.

<?php
class NodeBookMigration extends NodeMigration {
  public function __construct() {
    parent::__construct(array('type' => 'non-book', 'new type' => 'book'));
  }
  ...
}
?>
rsbecker’s picture

I guess he did include the answer. But that assumes you have a site that is book-centric.

If your site uses only some content types as book pages it's not terribly efficient to build your node migration around the book module.

I guess the alternative is to create two include files, one for content types that are used in books, the other for those not used in books. But I suspect it would be more efficient to take a more modular approach, perhaps making the complete function operate as a callback, or to make it operate on the appropriate content types after all content is migrated.

NobuT’s picture

WD node: PDOException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '5710' for key 'vid': INSERT INTO {node}

If you have seen an error like the above during the book migration, there's a way to work around it in addition to Neograph734's code. Replace the order by clause with this one.

    // Sort by depth so there is always a parent
    $orderby =& $query->getOrderBy();
    unset($orderby['changed']);
    $query->orderBy('ml.depth', 'ASC');

I've used the D2D migration (7.x-2.0) along with ajlozier's D2D code. Without removing the 'orderby changed' clause, the additional orderby ml.depth didn't take any effect, and child nodes were processed before the parent node.

KarlShea’s picture

I had the same problem. Here are my final query() and complete() functions:

public function query() {
    $query = parent::query();
    // Add in book parent child relationships.
    $query->join('book', 'b', 'n.nid = b.nid');
    $query->addField('b', 'bid', 'book_id');
    $query->join('menu_links', 'ml', 'b.mlid = ml.mlid');
    $query->addField('ml', 'weight', 'book_weight');

    // Get nid of parent
    $query->leftJoin('menu_links', 'mm', 'ml.plid = mm.mlid');
    $query->addField('mm', 'link_path', 'parent_id');

    // Remove order by changed
    $order = &$query->getOrderBy();
    unset($order['n.changed']);

    // Sort by depth so there is always a parent
    $query->orderBy('ml.depth');

    return $query;
  }
  public function complete($node, stdClass $row) {
    // If this is a top-level book, don't set a parent.
    $book_id = $row->nid == $row->book_id ? $node->nid : $this->lookupMigratedBook($row->book_id);
    // Book id - effectively, parent nid.
    $node->book['bid'] = $book_id;
    $node->book['nid'] = $node->nid;
    $node->book['weight'] = $row->book_weight;

    // We strip the 'node/'-part from the link_path
    $row->parent_id = str_replace('node/', '', $row->parent_id);

    // If there is a parent we lookup the migrated node
    if (isset($row->parent_id) && $row->parent_id > 0) {
      $parent_node = parent::handleSourceMigration($this->getMachineName(), $row->parent_id);

      // Get the mlid of the migrated parent
      $query = db_select('menu_links', 'ml')
        ->fields('ml')
        ->condition('ml.link_path', 'node/' . $parent_node, '=')
        ->execute()
        ->fetchAssoc();
      $plid = $query['mlid'];

      // We add the parent mlid (plid)
      $node->book['plid'] = $plid;
    }

    node_save($node);
  }
dankegel’s picture

I tried using this NodeMigrate base class with drupal 7.37, but "drush ms" complains

 The field mapping arguments() method is now deprecated - please use  [error]
 subfield notation instead.

presumably because of this line:

$this->addFieldMapping('body', 'body')->arguments($body_arguments);

Does this page need updating?

rsbecker’s picture

This has been true at least since migrate 2.4 or 2.5. You need to have a separate line for each sub-field, i.e.

  $this->addFieldMapping('body', 'body');
  $this->addFieldMapping('body:language')->defaultValue('en');
  $this->addFieldMapping('body:format')->defaultValue('filtered_html');
...

Or you need to put some/all subfields into the addUnmigratedDestinations array or set them as DNM.

dankegel’s picture

The example's NodeMigration class defines node_query(), but
the example's NodeBookMigration subclass defines NodeQuery().

Seems like NodeQuery's never going to get called, because it's overriding the wrong function?

pierregermain’s picture

Right now the migration of the book link hierarchy from D6 to D8 is still not working. I didn't find documentation about it. So I have created a workaround usting SQL (Mysql). I would like to share this with you. I hope you don't mind. For me this was a livesaver.

-- Level 1
INSERT INTO D8.`book` (`nid`, `bid`, `pid`, `has_children`, `weight`, `depth`, `p1`, `p2`, `p3`, `p4`, `p5`, `p6`, `p7`, `p8`, `p9`)
SELECT DISTINCT
    SUBSTRING(book.link_path, 6, 6) AS NID,
    SUBSTRING(book.menu_name, 10, 6) AS BID,
    book.plid AS PID,
    book.has_children AS has_children,
    book.weight AS weight,
    book.depth AS depth,
    SUBSTRING(m1.link_path, 6, 6) AS P1,
    book.p2 AS P2,
    book.p3 AS P3,
    book.p4 AS P4,
    book.p5 AS P5,
    book.p6 AS P6,
    book.p7 AS P7,
    book.p8 AS P8,
    book.p9 AS P9
FROM
    D6.`menu_links` AS book,
    `menu_links` AS m1
WHERE
    book.module = 'book'
    AND m1.module = 'book'
    AND book.p1 = m1.mlid
    and book.depth = 1

-- Level 2
INSERT INTO D8.`book`
(`nid`, `bid`, `pid`, `has_children`, `weight`, `depth`,
 `p1`, `p2`, `p3`, `p4`, `p5`, `p6`, `p7`, `p8`, `p9`)
SELECT DISTINCT
    SUBSTRING(book.link_path, 6, 6) AS NID,
    SUBSTRING(book.menu_name, 10, 6) AS BID,
    SUBSTRING(father.link_path, 6, 6) AS PID,
    book.has_children AS has_children,
    book.weight AS weight,
    book.depth AS depth,
    SUBSTRING(m1.link_path, 6, 6) AS P1,
    SUBSTRING(m2.link_path, 6, 6) AS P2,
    book.p3 AS P3,
    book.p4 AS P4,
    book.p5 AS P5,
    book.p6 AS P6,
    book.p7 AS P7,
    book.p8 AS P8,
    book.p9 AS P9
FROM
    D6.`menu_links` AS book,
    `menu_links` AS father,
    `menu_links` AS m1,
    `menu_links` AS m2
WHERE
    book.module = 'book'
    AND father.module = 'book'
    AND m1.module = 'book'
    AND book.plid = father.mlid
    AND book.p1 = m1.mlid
    AND book.p2 = m2.mlid
    AND m2.module = 'book'
    and book.depth = 2

-- Level 3
INSERT INTO D8.`book`
(`nid`, `bid`, `pid`, `has_children`, `weight`, `depth`,
 `p1`, `p2`, `p3`, `p4`, `p5`, `p6`, `p7`, `p8`, `p9`)
SELECT DISTINCT
    SUBSTRING(book.link_path, 6, 6) AS NID,
    SUBSTRING(book.menu_name, 10, 6) AS BID,
    SUBSTRING(father.link_path, 6, 6) AS PID,
    book.has_children AS has_children,
    book.weight AS weight,
    book.depth AS depth,
    SUBSTRING(m1.link_path, 6, 6) AS P1,
    SUBSTRING(m2.link_path, 6, 6) AS P2,
    SUBSTRING(m3.link_path, 6, 6) AS P3,
    book.p4 AS P4,
    book.p5 AS P5,
    book.p6 AS P6,
    book.p7 AS P7,
    book.p8 AS P8,
    book.p9 AS P9
FROM
    D6.`menu_links` AS book,
    `menu_links` AS father,
    `menu_links` AS m1,
    `menu_links` AS m2,
    `menu_links` AS m3
WHERE
    book.module = 'book'
    AND father.module = 'book'
    AND m1.module = 'book'
    -- AND book.link_path = 'node/395'
    AND book.plid = father.mlid
    AND book.p1 = m1.mlid
    AND book.p2 = m2.mlid
    AND m2.module = 'book'
    AND book.p3 = m3.mlid
    AND m3.module = 'book'
    and book.depth = 3

-- Level 4 to Level 9 is similar to Level 3 ... you should get the point :)