Issue #1004812 by bojanz, mikeryan: Add schema-drive table destination plugin
authorMike Ryan
Tue, 10 May 2011 22:36:46 +0000 (18:36 -0400)
committerMike Ryan
Tue, 10 May 2011 22:36:46 +0000 (18:36 -0400)
CHANGELOG.txt
migrate.info
migrate_example/migrate_example.install
migrate_example/wine.inc
migrate_example/wine.install.inc
plugins/destinations/table.inc [new file with mode: 0644]
tests/plugins/destinations/table.test [new file with mode: 0644]

index dad5b8f..2340de4 100644 (file)
@@ -16,6 +16,7 @@ before upgrading from Migrate 2.0 to this version if you have migrate_ui enabled
 
 Features and enhancements
 - #1017246 - Added support for running migrations from the dashboard.
+- #1004812 - Added schema-driven table destination plugin.
 - #1005090 - Added filefield property import from JSON input.
 - #730980 - Added more detailed reporting on import.
 - #1142384 - Extended file field support to copy from remote URLs.
index 137ed12..4985b67 100755 (executable)
@@ -24,6 +24,7 @@ files[] = plugins/destinations/comment.inc
 files[] = plugins/destinations/path.inc
 files[] = plugins/destinations/fields.inc
 files[] = plugins/destinations/profile.inc
+files[] = plugins/destinations/table.inc
 files[] = plugins/destinations/table_copy.inc
 files[] = plugins/sources/csv.inc
 files[] = plugins/sources/json.inc
@@ -35,6 +36,7 @@ files[] = plugins/sources/mssql.inc
 files[] = plugins/sources/xml.inc
 files[] = tests/plugins/destinations/comment.test   
 files[] = tests/plugins/destinations/node.test    
+files[] = tests/plugins/destinations/table.test    
 files[] = tests/plugins/destinations/term.test    
 files[] = tests/plugins/destinations/user.test    
 files[] = tests/plugins/sources/xml.test
index a2080df..407d164 100644 (file)
@@ -145,3 +145,16 @@ function migrate_example_update_6004() {
   $ret[] = t('Reconfigured sample data for file fields.');
   return $ret;
 }
+
+/**
+ * Sample data for table destinations.
+ */
+function migrate_example_update_6005() {
+  $ret = array();
+  db_create_table($ret, 'migrate_example_wine_table_source', migrate_example_wine_schema_table_source());
+  db_create_table($ret, 'migrate_example_wine_table_dest', migrate_example_wine_schema_table_dest());
+  migrate_example_wine_data_table_source();
+
+  $ret[] = t('Added sample data for table destinations.');
+  return $ret;
+}
index 82d183f..46e50c7 100644 (file)
@@ -578,6 +578,37 @@ class WineCommentMigration extends AdvancedExampleMigration {
   }
 }
 
+// TIP: An easy way to simply migrate into a Drupal table (i.e., one defined
+// through the Schema API) is to use the MigrateDestinationTable destination.
+// Just pass the table name to getKeySchema and the MigrateDestinationTable constructor.
+class WineTableMigration extends AdvancedExampleMigration {
+  public function __construct() {
+    parent::__construct();
+    $this->description = 'Miscellaneous table data';
+    $this->softDependencies = array('WineComment');
+    $table_name = 'migrate_example_wine_table_dest';
+    $this->map = new MigrateSQLMap($this->machineName,
+      array('fooid' => array(
+              'type' => 'int',
+              'unsigned' => TRUE,
+              'not null' => TRUE,
+             )
+           ),
+        MigrateDestinationTable::getKeySchema($table_name)
+      );
+    $query = db_select('migrate_example_wine_table_source', 't')
+             ->fields('t', array('fooid', 'field1', 'field2'));
+    $this->source = new MigrateSourceSQL($query);
+    $this->destination = new MigrateDestinationTable($table_name);
+
+    // Mapped fields
+    $this->addFieldMapping('drupal_text', 'field1');
+    $this->addFieldMapping('drupal_int', 'field2');
+
+    $this->addUnmigratedDestinations(array('recordid'));
+  }
+}
+
 class WineFinishMigration extends MigrationBase {
   public function __construct() {
     parent::__construct();
index cdeb3ab..9842f45 100644 (file)
@@ -18,6 +18,8 @@ function migrate_example_wine_schema() {
   $schema['migrate_example_wine_comment'] = migrate_example_wine_schema_comment();
   $schema['migrate_example_wine_comment_updates'] = migrate_example_wine_schema_comment_updates();
   $schema['migrate_example_wine_files'] = migrate_example_wine_schema_files();
+  $schema['migrate_example_wine_table_source'] = migrate_example_wine_schema_table_source();
+  $schema['migrate_example_wine_table_dest'] = migrate_example_wine_schema_table_dest();
 
   return $schema;
 }
@@ -40,6 +42,7 @@ function migrate_example_wine_install() {
   migrate_example_wine_data_category_producer();
   migrate_example_wine_data_comment();
   migrate_example_wine_data_comment_updates();
+  migrate_example_wine_data_table_source();
 }
 
 function migrate_example_wine_uninstall() {
@@ -564,6 +567,60 @@ function migrate_example_wine_schema_files() {
   );
 }
 
+function migrate_example_wine_schema_table_source() {
+  return array(
+    'description' => 'Source data to go into a custom Drupal table',
+    'fields' => array(
+      'fooid'  => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'description' => 'Primary key',
+      ),
+      'field1'  => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'description' => 'First field',
+      ),
+      'field2'  => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'description' => 'Second field',
+      ),
+    ),
+    'primary key' => array('fooid'),
+  );
+}
+
+function migrate_example_wine_schema_table_dest() {
+  return array(
+    'description' => 'Custom Drupal table to receive source data directly',
+    'fields' => array(
+      'recordid'  => array(
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'description' => 'Primary key',
+      ),
+      'drupal_text'  => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'description' => 'First field',
+      ),
+      'drupal_int'  => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'description' => 'Second field',
+      ),
+    ),
+    'primary key' => array('recordid'),
+  );
+}
+
 function migrate_example_wine_content_types() {
   // This code based on from standard.profile.
   // Insert default user-defined node types into the database.
@@ -1043,3 +1100,17 @@ function migrate_example_wine_data_files() {
              $row);
   }
 }
+
+function migrate_example_wine_data_table_source() {
+  $data = array(
+    array(3, 'Some sample data', 58),
+    array(15, 'Whatever', 2),
+    array(646, 'More sample data', 34989),
+  );
+  foreach ($data as $row) {
+    db_query("INSERT INTO {migrate_example_wine_table_source}
+              (fooid, field1, field2)
+              VALUES(%d, '%s', %d)",
+             $row);
+  }
+}
diff --git a/plugins/destinations/table.inc b/plugins/destinations/table.inc
new file mode 100644 (file)
index 0000000..e5f5a55
--- /dev/null
@@ -0,0 +1,197 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Support for tables defined through the Schema API.
+ */
+
+/**
+ * Destination class implementing migration into a single table defined through
+ * the Schema API.
+ */
+class MigrateDestinationTable extends MigrateDestination {
+  /**
+   * The schema of the current table.
+   *
+   * @var array
+   */
+  protected $schema = NULL;
+
+  /**
+   * The name of the current table.
+   *
+   * @var string
+   */
+  protected $tableName = NULL;
+
+  public function __construct($table_name) {
+    $this->schema = drupal_get_schema($table_name);
+    $this->tableName = $table_name;
+  }
+
+  static public function getKeySchema($table_name = NULL) {
+    if (empty($table_name)) {
+      return array();
+    }
+    $schema = drupal_get_schema($table_name);
+    $keys = array();
+    foreach ($schema['primary key'] as $primary_key) {
+      // We can't have any form of serial fields here, since the mapping table
+      // already has it's own.
+      $schema['fields'][$primary_key]['auto_increment'] = FALSE;
+      if ($schema['fields'][$primary_key]['type'] == 'serial') {
+        $schema['fields'][$primary_key]['type'] = 'int';
+      }
+
+      $keys[$primary_key] = $schema['fields'][$primary_key];
+    }
+
+    return $keys;
+  }
+
+  public function __toString() {
+    $output = t('Table !name', array('!name' => $this->tableName));
+    return $output;
+  }
+
+  /**
+   * Delete a single row.
+   *
+   * @param $id
+   *  Primary key(s) and their values.
+   */
+  public function rollback(array $id) {
+    migrate_instrument_start('table rollback');
+    $delete = db_delete($this->tableName);
+    foreach ($id as $key => $value) {
+      $delete->condition($key, $value);
+    }
+    $delete->execute();
+    migrate_instrument_stop('table rollback');
+  }
+
+  /**
+   * Import a single row.
+   *
+   * @param $entity
+   *  Object object to build. Prefilled with any fields mapped in the Migration.
+   * @param $row
+   *  Raw source data object - passed through to prepare/complete handlers.
+   * @return array
+   *  Array of key fields of the object that was saved if
+   *  successful. FALSE on failure.
+   */
+  public function import(stdClass $entity, stdClass $row) {
+    if (empty($this->schema['primary key'])) {
+      throw new MigrateException(t("The destination table has no primary key defined."));
+    }
+
+    // Only filled when doing an update.
+    $primary_key = array();
+
+    $migration = Migration::currentMigration();
+    // Updating previously-migrated content?
+    if (isset($row->migrate_map_destid1)) {
+      $i = 1;
+      foreach ($this->schema['primary key'] as $key) {
+        $destination_id = $row->{'migrate_map_destid' . $i};
+        if (isset($entity->{$key})) {
+          if ($entity->{$key} != $destination_id) {
+            throw new MigrateException(t("Incoming id !id and map destination id !destid don't match",
+              array('!id' => $entity->{$key}, '!destid' => $destination_id)));
+          }
+        }
+        else {
+          $entity->{$key} = $destination_id;
+        }
+        $i++;
+      }
+    }
+
+    if ($migration->getSystemOfRecord() == Migration::DESTINATION) {
+      foreach ($this->schema['primary key'] as $key) {
+        if (!isset($entity->{$key})) {
+          throw new MigrateException(t('System-of-record is DESTINATION, but no destination id provided'));
+        }
+      }
+
+      $select = db_select($this->tableName);
+      foreach ($this->schema['primary key'] as $key) {
+        $select->condition($key, $entity->{$key});
+      }
+      $old_entity = $select->execute()->fetchObject();
+
+      foreach ($entity as $field => $value) {
+        $old_entity->$field = $entity->$field;
+      }
+      $entity = $old_entity;
+    }
+
+    $this->prepare($entity, $row);
+    $status = drupal_write_record($this->tableName, $entity, $primary_key);
+    $this->complete($entity, $row);
+
+    if ($status) {
+      $id = array();
+      foreach ($this->schema['primary key'] as $key) {
+        $id[] = $entity->{$key};
+      }
+
+      return $id;
+    }
+  }
+
+  /**
+   * Returns a list of fields available to be mapped.
+   *
+   * @return array
+   *  Keys: machine names of the fields (to be passed to addFieldMapping)
+   *  Values: Human-friendly descriptions of the fields.
+   */
+  public function fields() {
+    $fields = array();
+    foreach ($this->schema['fields'] as $column => $schema) {
+      $fields[$column] = t('Type: !type', array('!type' => $schema['type']));
+    }
+    return $fields;
+  }
+
+  /**
+   * Give handlers a shot at modifying the object before saving it.
+   *
+   * @param $entity
+   *  Entity object to build. Prefilled with any fields mapped in the Migration.
+   * @param $source_row
+   *  Raw source data object - passed through to prepare handlers.
+   */
+  public function prepare($entity, stdClass $source_row) {
+    $migration = Migration::currentMigration();
+    $entity->migrate = array(
+      'machineName' => $migration->getMachineName(),
+    );
+
+    // Call any prepare handler for this specific Migration.
+    if (method_exists($migration, 'prepare')) {
+      $migration->prepare($entity, $source_row);
+    }
+  }
+
+  /**
+   * Give handlers a shot at modifying the object (or taking additional action)
+   * after saving it.
+   *
+   * @param $object
+   *  Entity object to build. This is the complete object after saving.
+   * @param $source_row
+   *  Raw source data object - passed through to complete handlers.
+   */
+  public function complete($entity, stdClass $source_row) {
+    $migration = Migration::currentMigration();
+
+    // Call any complete handler for this specific Migration.
+    if (method_exists($migration, 'complete')) {
+      $migration->complete($entity, $source_row);
+    }
+  }
+}
diff --git a/tests/plugins/destinations/table.test b/tests/plugins/destinations/table.test
new file mode 100644 (file)
index 0000000..275b741
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * Test table migration.
+ */
+class MigrateTableUnitTest extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Table migration',
+      'description' => 'Test migration of table data',
+      'group' => 'Migrate',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('autoload', 'dbtng', 'taxonomy', 'content', 'text', 'number',
+      'date_api', 'date_timezone', 'date', 'filefield', 'imagefield',
+      'migrate', 'migrate_extras', 'migrate_example');
+    drupal_flush_all_caches();
+  }
+
+  function testTableImport() {
+    $migration = Migration::getInstance('WineTable');
+    $result = $migration->processImport();
+    $this->assertEqual($result, Migration::RESULT_COMPLETED,
+      t('Table import returned RESULT_COMPLETED'));
+
+    $result = db_query(
+      "SELECT COUNT(*)
+       FROM {migrate_example_wine_table_source} s
+       INNER JOIN {migrate_map_winetable} map ON s.fooid=map.sourceid1
+       INNER JOIN {migrate_example_wine_table_dest} d ON map.destid1=d.recordid"
+    );
+
+    $this->assertEqual(db_result($result), 3,
+      t('Count of imported records is correct'));
+
+    // Test rollback
+/* Implement when http://drupal.org/node/1147366 is fixed
+    $result = $migration->processRollback();
+    $this->assertEqual($result, Migration::RESULT_COMPLETED,
+      t('Variety term rollback returned RESULT_COMPLETED'));*/
+  }
+}