#!/usr/bin/php * * SQL Migrate * This tool allow to migrate the data from one SQL database to another. * The DB engine can also be different. * * You must have the source DB and create a target DB with all the destination * tables available. * You must pass one parameter : the configuration file where all the * informations will be stored. The file will be created if it doesn't exists. */ require_once(__DIR__ . "/../src/Console.php"); require_once(__DIR__ . "/../src/Dblayeroo.php"); require_once(__DIR__ . "/../src/Getopts.php"); require_once(__DIR__ . "/../src/Config.php"); /** * Manage the configuration file */ class Configuration extends \Domframework\Config { /** * The constructor of the configuration set the differents params */ public function __construct() { $this->default = [ "db" => [ "srcDSN" => "", "srcUser" => null, "srcPassword" => null, "dstDSN" => "", "dstUser" => null, "dstPassword" => null, ], "tables" => [ ], "tablesRel" => [ ], "fields" => [ ], "tablesOrder" => [ ], "deleteContent" => null, ]; } } /** * The main class */ class Main { /** * The constructor of the class do the main job */ public function __construct() { // Manage the Getopts $getopts = new Domframework\Getopts(); $getopts->add("Help", "?h", ["help"], "Help of the software"); $getopts->add( "Step", "", ["step:"], "Restart at provided step", "StepIdentifier" ); $getopts->add("Debug", "", ["debug"], "Display the SQL requests"); $getopts->add( "Execute", "", ["execute"], "Execute the configuration without requesting data (start at step 5)" ); if ($getopts->get("Help")) { die($getopts->help()); } if ($getopts->get("Step")) { if ( intval($getopts->get("Step")) < 0 || intval($getopts->get("Step")) > 6 ) { die("Invalid Step provided (Must be between 0 and 6)\n"); } } if (empty($getopts->restOfLine())) { throw new \Exception("No configuration file provided"); } if (count($getopts->restOfLine()) > 1) { throw new \Exception("Too much configuration file provided"); } $configurationFile = current($getopts->restOfLine()); if (! file_exists($configurationFile)) { touch($configurationFile); } $config = new Domframework\Configuration(); $config->confFile = $configurationFile; $db = $config->get("db"); $tables = $config->get("tables"); $tablesRel = $config->get("tablesRel"); $fields = $config->get("fields"); $tablesOrder = $config->get("tablesOrder"); $deleteContent = $config->get("deleteContent"); if ($db["srcDSN"] === "" || $db["dstDSN"] === "") { $step = 0; } elseif (empty($tables)) { $step = 1; } elseif (empty($tablesRel)) { $step = 2; } elseif (empty($fields)) { $step = 3; } elseif (empty($tablesOrder)) { $step = 4; } elseif ($deleteContent === null) { $step = 5; } else { $step = 6; } if ($getopts->get("Step")) { $step = intval($getopts->get("Step")); } if ($getopts->get("Execute")) { $step = 5; } if ($step === 0) { // Step 0: ask the connection parameters echo "#### Entering step 0 : ask the connection parameters\n"; $srcDSN = $this->ask("Enter SRC DSN"); $srcUser = $this->ask("Enter SRC User"); $srcPassword = $this->ask("Enter SRC Password"); $dstDSN = $this->ask("Enter DST DSN"); $dstUser = $this->ask("Enter DST User"); $dstPassword = $this->ask("Enter DST Password"); if ($srcUser === "") { $srcUser = null; $srcPassword = null; } if ($dstUser === "") { $dstUser = null; $dstPassword = null; } $value = [ "srcDSN" => $srcDSN, "srcUser" => $srcUser, "srcPassword" => $srcPassword, "dstDSN" => $dstDSN, "dstUser" => $dstUser, "dstPassword" => $dstPassword,]; $config->set("db", $value); $db = $config->get("db"); $step++; } if ($step === 1) { echo "#### Entering step 1: read the existing databases schemes\n"; $srcDB = new Domframework\Dblayeroo( $db["srcDSN"], $db["srcUser"], $db["srcPassword"] ); $srcTables = $srcDB->listTables(); $dstDB = new Domframework\Dblayeroo( $db["dstDSN"], $db["dstUser"], $db["dstPassword"] ); $dstTables = $dstDB->listTables(); $value = [ "srcTables" => $srcTables, "dstTables" => $dstTables, ]; $config->set("tables", $value); $step++; echo "\n"; } echo "#### Connect to the defined databases\n"; $srcDB = new Domframework\Dblayeroo( $db["srcDSN"], $db["srcUser"], $db["srcPassword"] ); $dstDB = new Domframework\Dblayeroo( $db["dstDSN"], $db["dstUser"], $db["dstPassword"] ); echo "\n"; if ($step == 2) { echo "#### Entering step 2: ask the relations between tables\n"; $tables = $config->get("tables"); echo "# For each destination table, please provide the source table\n"; sort($tables["srcTables"]); echo "# Allowed source tables : " . implode(", ", $tables["srcTables"]) . "\n"; $value = []; natsort($tables["dstTables"]); foreach ($tables["dstTables"] as $table) { while (1) { $value[$table] = $this->ask( "Destination Table '$table' filled by source table" ); if ( in_array($value[$table], $tables["srcTables"]) || $value[$table] === "" ) { continue 2; } $this->error("The table '" . $value[$table] . " doesn't exists"); echo "# Allowed source tables : " . implode(", ", $tables["srcTables"]) . "\n"; } } $config->set("tablesRel", $value); $step++; echo "\n"; } $tablesRel = $config->get("tablesRel"); if ($step === 3) { echo "#### Entering step 3: ask the relation between fields\n"; echo "# For each destination table, if a source table is provided,\n" . "# for each field, provide the source SQL request (can be field,\n" . "# CONCAT(), text between simple quotes, null...)\n"; if ($config->get("fields")) { $value = $config->get("fields"); } else { $value = []; } foreach ($tablesRel as $dstTable => $srcTable) { echo "- Destination Table '$dstTable': "; if ($srcTable === "") { echo "No source table. SKIPPED\n"; continue; } echo "\n"; $dstSchema = $dstDB->getTableSchema($dstTable); $srcSchema = $srcDB->getTableSchema($srcTable); echo " List of the allowed fields: " . implode(", ", array_keys($srcSchema["fields"])) . "\n"; foreach (array_keys($dstSchema["fields"]) as $field) { if (! key_exists($dstTable, $value)) { $value[$dstTable] = []; } if (! key_exists($field, $value[$dstTable])) { $value[$dstTable][$field] = ""; } while (1) { $oldval = (isset($config->get("fields")[$dstTable][$field])) ? $config->get("fields")[$dstTable][$field] : ""; $answer = $this->ask( "Destination Table '$dstTable' field '$field'", $oldval ); $value[$dstTable][$field] = $answer; if (trim($value[$dstTable][$field]) !== "") { break; } // TODO : Check if the field is valid (think about function) $this->error("The field source must be defined !"); } } $config->set("fields", $value); } $step++; echo "\n"; } $fields = $config->get("fields"); if ($step === 4) { echo "#### Entering step 4: Order the tables by foreign keys\n"; echo "# The tables with foreign keys constraint must be populated \n" . "# after the tables depending of them\n"; $tablesToOrder = []; foreach ($tablesRel as $dstTable => $srcTable) { if ($srcTable !== "") { $tablesToOrder[] = $dstTable; } } while (1) { echo "# Here are the tables to order: " . implode(",", $tablesToOrder) . "\n"; $tablesOrder = $this->ask("Order the tables, separated by commas"); $tablesOrder = explode(",", $tablesOrder); if (count($tablesToOrder) === count($tablesOrder)) { break; } $this->error("Not all the tables are ordered"); } $config->set("tablesOrder", $tablesOrder); $step++; echo "\n"; } $tablesOrder = $config->get("tablesOrder"); if ($step === 5) { if ($deleteContent === null) { echo "# The content of the destination tables can be deleted before\n" . "# adding the source data.\n"; echo "# Attention: the order is important if there is foreign keys!\n" . "# Remove the most constraint tables before deleting the less\n" . "# constraint ones\n"; echo "# List of the destination tables: " . implode(",", array_keys(array_diff($tablesRel, [""]))) . "\n"; $deleteContent = $this->ask("Get the list of the tables to be " . "cleaned, separated by comas"); $config->set("deleteContent", $deleteContent); $deleteContent = $config->get("deleteContent"); } } if ($deleteContent !== "") { echo "#### Entering step 5: delete tables content\n"; foreach (explode(",", $deleteContent) as $table) { $table = trim($table); echo "- Delete content of table '$table'\n"; $dstDB->table($table)->unique([])->delete()->execute(); } $step = 6; echo "\n"; } else { echo "#### Entering step5: Skipped delete tables content\n"; $step = 6; } if ($step === 6) { echo "#### Entering step 6: populate the destination database with the " . "configured data\n"; foreach ($tablesOrder as $dstTable) { echo "- Populate table $dstTable:\n"; // Create the source SQL request. Add the alias name of the destination // table to allow to import directely in the destination $sql = "SELECT "; $i = 0; foreach ($fields[$dstTable] as $key => $val) { if ($i > 0) { $sql .= ","; } $sql .= "$val AS $key"; $i++; } $sql .= " FROM " . $tablesRel[$dstTable]; echo " $sql\n"; echo " "; // Define all the variables needed to insert $dstSchema = $dstDB->getTableSchema($dstTable); $dstDB->table($dstTable); $dstDB->fields($dstSchema["fields"]); $dstDB->primary($dstSchema["primary"]); foreach ($srcDB->directQuery($sql) as $row) { //print_r ($row); $dstDB->clearRequest(); $dstDB->insert() ->setValues($row); if (! $getopts->get("Debug")) { echo "."; } else { echo $dstDB->getDisplayQuery(); } $dstDB->execute(); } echo "\n"; } $step++; } // Last step echo "#### Entering step $step: End of process\n"; } /** * Ask a question to the user and return the answer * @param string $question the question message * @param string|null $proposal The optional pre-filled proposal * @return string The answer */ public function ask($question, $proposal = "") { $console = new Domframework\Console(); $console->echo("$question: "); return trim($console->readline($proposal)); } /** * Display an error to the user * @param string $msg The error message to display */ public function error($msg) { file_put_contents("php://stderr", "ERROR: " . $msg . "\n"); } } try { $main = new \main(); } catch (Exception $e) { file_put_contents("php://stderr", $e->getMessage() . "\n"); exit(4); }