diff --git a/Tests/Mysql/MysqlExporterTest.php b/Tests/Mysql/MysqlExporterTest.php index c099e9a82..98d0db59d 100644 --- a/Tests/Mysql/MysqlExporterTest.php +++ b/Tests/Mysql/MysqlExporterTest.php @@ -13,7 +13,7 @@ * * @since 1.0 */ -class MysqlExporterText extends TestCase +class MysqlExporterTest extends TestCase { /** * @var object The mocked database object for use by test methods. @@ -215,7 +215,7 @@ public function test__toString() - + '; @@ -278,7 +278,7 @@ public function testBuildXml() - + '; @@ -318,7 +318,7 @@ public function testBuildXmlStructure() ' ', ' ', ' ', + 'Null="" Index_type="BTREE" Sub_part="" Comment="" />', ' ' ) ), diff --git a/Tests/Pgsql/PgsqlDriverTest.php b/Tests/Pgsql/PgsqlDriverTest.php index 41a6a6e64..c646e7107 100644 --- a/Tests/Pgsql/PgsqlDriverTest.php +++ b/Tests/Pgsql/PgsqlDriverTest.php @@ -360,24 +360,28 @@ public function testGetTableKeys() $pkey->idxName = 'assets_pkey'; $pkey->isPrimary = true; $pkey->isUnique = true; + $pkey->indKey = '1'; $pkey->Query = 'ALTER TABLE assets ADD PRIMARY KEY (id)'; $asset = new \stdClass; $asset->idxName = 'idx_asset_name'; $asset->isPrimary = false; $asset->isUnique = true; + $asset->indKey = '6'; $asset->Query = 'CREATE UNIQUE INDEX idx_asset_name ON assets USING btree (name)'; $lftrgt = new \stdClass; $lftrgt->idxName = 'assets_idx_lft_rgt'; $lftrgt->isPrimary = false; $lftrgt->isUnique = false; + $lftrgt->indKey = '3 4'; $lftrgt->Query = 'CREATE INDEX assets_idx_lft_rgt ON assets USING btree (lft, rgt)'; $id = new \stdClass; $id->idxName = 'assets_idx_parent_id'; $id->isPrimary = false; $id->isUnique = false; + $id->indKey = '2'; $id->Query = 'CREATE INDEX assets_idx_parent_id ON assets USING btree (parent_id)'; $this->assertThat(self::$driver->getTableKeys('assets'), $this->equalTo(array($pkey, $id, $lftrgt, $asset)), __LINE__); diff --git a/Tests/Pgsql/PgsqlExporterTest.php b/Tests/Pgsql/PgsqlExporterTest.php index 645fdce08..dc6f7d0d6 100644 --- a/Tests/Pgsql/PgsqlExporterTest.php +++ b/Tests/Pgsql/PgsqlExporterTest.php @@ -48,6 +48,9 @@ protected function setup() 'getTableColumns', 'getTableKeys', 'getTableSequences', + 'getSequenceLastValue', + 'getSequenceIsCalled', + 'getNamesKey', 'getVersion', 'quoteName', 'loadObjectList', @@ -77,28 +80,28 @@ protected function setup() 'column_name' => 'id', 'type' => 'integer', 'null' => 'NO', - 'default' => 'nextval(\'jos_dbtest_id_seq\'::regclass)', + 'Default' => 'nextval(\'jos_dbtest_id_seq\'::regclass)', 'comments' => '', ), (object) array( 'column_name' => 'title', 'type' => 'character varying(50)', 'null' => 'NO', - 'default' => 'NULL', + 'Default' => 'NULL', 'comments' => '', ), (object) array( 'column_name' => 'start_date', 'type' => 'timestamp without time zone', 'null' => 'NO', - 'default' => 'NULL', + 'Default' => 'NULL', 'comments' => '', ), (object) array( 'column_name' => 'description', 'type' => 'text', 'null' => 'NO', - 'default' => 'NULL', + 'Default' => 'NULL', 'comments' => '', ) ) @@ -116,12 +119,43 @@ protected function setup() 'idxName' => 'jos_dbtest_pkey', 'isPrimary' => 'TRUE', 'isUnique' => 'TRUE', + 'indKey' => '1', 'Query' => 'ALTER TABLE "jos_dbtest" ADD PRIMARY KEY (id)', ) ) ) ); + $this->dbo->expects( + $this->any() + ) + ->method('getNamesKey') + ->will( + $this->returnValue( + 'id' + ) + ); + + $this->dbo->expects( + $this->any() + ) + ->method('getSequenceLastValue') + ->will( + $this->returnValue( + '1' + ) + ); + + $this->dbo->expects( + $this->any() + ) + ->method('getSequenceIsCalled') + ->will( + $this->returnValue( + 'f' + ) + ); + /* Check if database is at least 9.1.0 */ $this->dbo->expects( $this->any() @@ -243,7 +277,7 @@ public function test__toString() // Set up the export settings. $instance ->setDbo($this->dbo) - ->from('jos_test') + ->from('jos_dbtest') ->withStructure(true); /* Depending on which version is running, 9.1.0 or older */ @@ -252,14 +286,14 @@ public function test__toString() $expecting = ' - - + + - + '; @@ -314,7 +348,7 @@ public function testBuildXml() // Set up the export settings. $instance ->setDbo($this->dbo) - ->from('jos_test') + ->from('jos_dbtest') ->withStructure(true); /* Depending on which version is running, 9.1.0 or older */ @@ -323,14 +357,14 @@ public function testBuildXml() $expecting = ' - - + + - + '; @@ -359,7 +393,7 @@ public function testBuildXmlStructure() // Set up the export settings. $instance ->setDbo($this->dbo) - ->from('jos_test') + ->from('jos_dbtest') ->withStructure(true); /* Depending on which version is running, 9.1.0 or older */ @@ -369,14 +403,14 @@ public function testBuildXmlStructure() $instance->buildXmlStructure(), $this->equalTo( array( - ' ', - ' ', + ' ', + ' ', ' ', ' ', ' ', ' ', - ' ', + ' ', ' ' ) ), @@ -542,8 +576,8 @@ public function testGetGenericTableName() $instance->setDbo($this->dbo); $this->assertThat( - $instance->getGenericTableName('jos_test'), - $this->equalTo('#__test'), + $instance->getGenericTableName('jos_dbtest'), + $this->equalTo('#__dbtest'), 'The testGetGenericTableName should replace the database prefix with #__.' ); } diff --git a/Tests/Pgsql/PgsqlImporterTest.php b/Tests/Pgsql/PgsqlImporterTest.php index 5a212ce07..7d4a2fb91 100644 --- a/Tests/Pgsql/PgsqlImporterTest.php +++ b/Tests/Pgsql/PgsqlImporterTest.php @@ -48,6 +48,7 @@ public function setup() 'getTableColumns', 'getTableKeys', 'getTableSequences', + 'getSetvalSequenceSql', 'getVersion', 'quote', 'quoteName', @@ -103,12 +104,14 @@ public function setup() 'Index' => 'jos_dbtest_pkey', 'is_primary' => 'TRUE', 'is_unique' => 'TRUE', + 'Key_name' => 'id', 'Query' => 'ALTER TABLE jos_dbtest ADD PRIMARY KEY (id)', ), (object) array( 'Index' => 'jos_dbtest_idx_name', 'is_primary' => 'FALSE', 'is_unique' => 'FALSE', + 'Key_name' => 'name', 'Query' => 'CREATE INDEX jos_dbtest_idx_name ON jos_dbtest USING btree (name)', ) ) @@ -261,26 +264,27 @@ public function dataGetAlterTableSql() $f3 = ''; $f2_def = ''; - $k1 = ''; - $k2 = ''; + $k2 = ''; - $k3 = ''; - $k4 = ''; - $pk = ''; $s1 = ''; $s2 = ''; + 'Max_Value="9223372036854775807" Last_Value="1" Increment="1" Cycle_option="NO" />'; - $addSequence = 'CREATE SEQUENCE jos_dbtest_title_seq INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 ' . + $addSequence = 'CREATE SEQUENCE IF NOT EXISTS jos_dbtest_title_seq INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 ' . 'NO CYCLE OWNED BY "public.jos_dbtest.title"'; + $setValSequence = 'SELECT setval(\'jos_dbtest_title_seq\', 1, FALSE)'; $changeCol = 'ALTER TABLE "jos_test" ALTER COLUMN "title" TYPE character ' . "varying(50),\nALTER COLUMN \"title\" SET NOT NULL,\nALTER COLUMN \"title\" SET DEFAULT 'add default'"; - $changeSeq = 'CREATE SEQUENCE jos_dbtest_title_seq INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 ' . + $changeSeq = 'CREATE SEQUENCE IF NOT EXISTS jos_dbtest_title_seq INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 ' . 'START 1 NO CYCLE OWNED BY "public.jos_dbtest.title"'; return array( @@ -317,6 +321,7 @@ public function dataGetAlterTableSql() new \SimpleXmlElement('' . $s1 . $s2 . $f1 . $f2 . $k1 . $k2 . ''), array( $addSequence, + $setValSequence, ), 'getAlterTableSQL should add the new sequence.' ), @@ -371,6 +376,7 @@ public function dataGetAlterTableSql() new \SimpleXmlElement('' . $s2 . $f1 . $f2 . $k1 . $k2 . ''), array( $changeSeq, + $setValSequence, 'DROP SEQUENCE "jos_dbtest_id_seq"',), 'getAlterTableSQL should change sequence.' ), @@ -406,9 +412,9 @@ public function dataGetColumnSql() { $sample = array( 'xml-id-field' => '', - 'xml-title-field' => '', + 'xml-title-field' => '', 'xml-title-def' => '', - 'xml-body-field' => '',); + 'xml-body-field' => '',); return array( array( @@ -602,7 +608,7 @@ public function testGetAddColumnSql() $instance->setDbo($this->dbo); $sample = array( - 'xml-title-field' => '', + 'xml-title-field' => '', 'xml-title-def' => '', 'xml-int-defnum' => '',); @@ -662,7 +668,7 @@ public function testGetAddSequenceSql() new \SimpleXmlElement($xmlIdSeq) ), $this->equalTo( - 'CREATE SEQUENCE jos_dbtest_id_seq INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 NO CYCLE OWNED BY "public.jos_dbtest.id"' + 'CREATE SEQUENCE IF NOT EXISTS jos_dbtest_id_seq INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 NO CYCLE OWNED BY "public.jos_dbtest.id"' ), 'getAddSequenceSQL did not yield the expected result.' ); @@ -677,9 +683,9 @@ public function testGetAddSequenceSql() */ public function testGetAddIndexSql() { - $xmlIndex = ''; - $xmlPrimaryKey = ''; $instance = new PgsqlImporterInspector; diff --git a/composer.json b/composer.json index 22990a6a6..f4e4ad28b 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,9 @@ }, "require-dev": { "joomla/coding-standards": "~2.0@alpha", + "joomla/console": "~2.0", + "joomla/archive": "~2.0@dev", + "joomla/filesystem": "~2.0@dev", "joomla/di": "~1.0|~2.0", "joomla/registry": "^1.4.5|~2.0", "joomla/test": "~1.2", @@ -19,6 +22,7 @@ "phpunit/dbunit": "~3.0" }, "suggest": { + "joomla/console": "If you want to use the ExporterCommand and ImporterCommand classes, please install joomla/console:~2.0", "joomla/di": "To use the Database ServiceProviderInterface objects, install joomla/di.", "joomla/registry": "To use the Database ServiceProviderInterface objects, install joomla/registry.", "ext-mysqli": "To connect to a MySQL database via MySQLi", diff --git a/src/Command/ExportCommand.php b/src/Command/ExportCommand.php new file mode 100644 index 000000000..3309d0719 --- /dev/null +++ b/src/Command/ExportCommand.php @@ -0,0 +1,159 @@ +db = $db; + + parent::__construct(); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since __DEPLOY_VERSION__ + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $symfonyStyle = new SymfonyStyle($input, $output); + + $symfonyStyle->title('Exporting Database'); + + $total_time = microtime(true); + $date = getDate(); + $dateFormat = sprintf("%s-%02s-%02s",$date['year'], $date['mon'], $date['mday']); + + $tables = $this->db->getTableList(); + $prefix = $this->db->getPrefix(); + $exp = $this->db->getExporter()->withStructure(); + + $folderPath = $input->getOption('folder'); + $tableName = $input->getOption('table'); + $all = $input->getOption('all'); + $zip = $input->getOption('zip'); + + $zipFile = $folderPath . '/' . 'data_exported_' . $dateFormat . '.zip'; + + if (($tableName == null) && ($all == null)) + { + $symfonyStyle->warning("Either the --table or --all option must be specified"); + return 1; + } + + if($tableName) + { + if (!\in_array($tableName, $tables)) + { + $symfonyStyle->error($tableName . ' does not exist in the database.'); + return 1; + } + $tables = array($tableName); + } + + if ($zip) + { + $archive = new Archive; + $zipArchive = $archive->getAdapter('zip'); + } + + foreach ($tables as $table) + { + if (strpos(substr($table, 0, strlen($prefix)), $prefix) !== false) + { + $task_i_time = microtime(true); + $filename = $folderPath . '/' . $table . '.xml'; + $symfonyStyle->newLine(); + $symfonyStyle->text('Exporting ' . $table . '....'); + $data = (string) $exp->from($table)->withData(true); + + if (file_exists($filename)) + { + File::delete($filename); + } + + File::write($filename, $data); + + if ($zip) + { + $zipFilesArray[] = array('name' => $table . '.xml', 'data' => $data); + $zipArchive->create($zipFile, $zipFilesArray); + File::delete($filename); + } + + $symfonyStyle->text('Exported in ' . round(microtime(true) - $task_i_time, 3)); + } + } + + $symfonyStyle->text('Total time: ' . round(microtime(true) - $total_time, 3)); + $symfonyStyle->success('Finished Exporting Database'); + + return 0; + } + + /** + * Configure the command. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function configure() + { + $this->setDescription('Exports the database'); + $this->addOption('folder', null, InputOption::VALUE_OPTIONAL, 'Dump in folder path', '.'); + $this->addOption('table', null, InputOption::VALUE_REQUIRED, 'Dump table name'); + $this->addOption('all', null, InputOption::VALUE_NONE, 'Dump all tables'); + $this->addOption('zip', null, InputOption::VALUE_NONE, 'Dump in zip format'); + } +} diff --git a/src/Command/ImportCommand.php b/src/Command/ImportCommand.php new file mode 100644 index 000000000..8df689e55 --- /dev/null +++ b/src/Command/ImportCommand.php @@ -0,0 +1,172 @@ +db = $db; + + parent::__construct(); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since __DEPLOY_VERSION__ + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $symfonyStyle = new SymfonyStyle($input, $output); + + $symfonyStyle->title('Importing Database'); + + $total_time = microtime(true); + + $folderPath = $input->getOption('folder'); + $tableName = $input->getOption('table'); + $all = $input->getOption('all'); + + $tables = Folder::files($folderPath, '\.xml$'); + + if (($tableName == null) && ($all == null)) + { + $symfonyStyle->warning("Either the --table or --all option must be specified"); + return 1; + } + + if ($tableName) + { + $tables = array($tableName . '.xml'); + } + + foreach ($tables as $table) + { + $task_i_time = microtime(true); + $percorso = $folderPath . '/' . $table; + + // Check file + if (!file_exists($percorso)) + { + $symfonyStyle->error('Not Found ' . $table . '....'); + return 1; + } + + $table_name = str_replace('.xml', '', $table); + $symfonyStyle->text('Importing ' . $table_name . ' from ' . $table); + + try + { + $imp = $this->db->getImporter()->from(file_get_contents($percorso))->withStructure()->asXml(); + } + catch (\Exception $e) + { + $symfonyStyle->error('Error on getImporter' . $table . ' ' . $e); + return 1; + } + + $symfonyStyle->text('Reading data from ' . $table); + + try + { + $symfonyStyle->text('Drop ' . $table_name); + $this->db->dropTable($table_name, true); + } + catch (ExecutionFailureException $e) + { + $symfonyStyle->error(' Error in DROP TABLE ' . $table_name . ' ' . $e); + return 1; + } + + try + { + $imp->mergeStructure(); + } + catch (\Exception $e) + { + $symfonyStyle->error('Error on mergeStructure' . $table . ' ' . $e); + return 1; + } + + $symfonyStyle->text('Checked structure ' . $table); + + try + { + $imp->importData(); + } + catch (\Exception $e) + { + $symfonyStyle->error('Error on importData' . $table . ' ' . $e); + return 1; + } + $symfonyStyle->text('Data loaded ' . $table . ' in ' . round(microtime(true) - $task_i_time, 3)); + $symfonyStyle->newLine(); + } + + $symfonyStyle->text('Total time: ' . round(microtime(true) - $total_time, 3)); + } + + /** + * Configure the command. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function configure() + { + $this->setDescription('Import the database'); + $this->addOption('folder', null, InputOption::VALUE_OPTIONAL, 'Import from folder path', '.'); + $this->addOption('table', null, InputOption::VALUE_REQUIRED, 'Import table name'); + $this->addOption('all', null, InputOption::VALUE_NONE, 'Import all files'); + } +} diff --git a/src/DatabaseExporter.php b/src/DatabaseExporter.php index de0c57b4b..7916fcde0 100644 --- a/src/DatabaseExporter.php +++ b/src/DatabaseExporter.php @@ -68,8 +68,9 @@ public function __construct() // Set up the class defaults: - // Export with only structure + // Export not only structure $this->withStructure(); + $this->withData(); // Export as xml. $this->asXml(); @@ -233,4 +234,89 @@ public function withStructure($setting = true) return $this; } + + /** + * Sets an internal option to export the data of the input table(s). + * + * @param boolean $setting True to export the data, false to not. + * + * @return $this + * + * @since __DEPLOY_VERSION__ + */ + public function withData($setting = false) + { + $this->options->withData = (boolean) $setting; + + return $this; + } + + /** + * Builds the XML data to export. + * + * @return array An array of XML lines (strings). + * + * @since __DEPLOY_VERSION__ + * @throws \Exception if an error occurs. + */ + protected function buildXmlData() + { + $buffer = array(); + + foreach ($this->from as $table) + { + // Replace the magic prefix if found. + $table = $this->getGenericTableName($table); + + // Get the details columns information. + $fields = $this->db->getTableColumns($table, false); + $colblob = array(); + + foreach ($fields as $field) + { + // Cacth blob for conversion xml + if ($field->Type == 'mediumblob') + { + $colblob[] = $field->Field; + } + } + + $query = $this->db->getQuery(true); + $query->select($query->quoteName(array_keys($fields))) + ->from($query->quoteName($table)); + $this->db->setQuery($query); + + $rows = $this->db->loadObjectList(); + + if (!count($rows)) + { + continue; + } + + $buffer[] = ' '; + + foreach ($rows as $row) + { + $buffer[] = ' '; + + foreach ($row as $key => $value) + { + if (!in_array($key, $colblob)) + { + $buffer[] = ' ' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . ''; + } + else + { + $buffer[] = ' ' . base64_encode($value) . ''; + } + } + + $buffer[] = ' '; + } + + $buffer[] = ' '; + } + + return $buffer; + } } diff --git a/src/DatabaseImporter.php b/src/DatabaseImporter.php index effd1a108..d709c6369 100644 --- a/src/DatabaseImporter.php +++ b/src/DatabaseImporter.php @@ -218,6 +218,53 @@ protected function getRealTableName($table) return $table; } + /** + * Import the data from the source into the existing tables. + * + * @return void + * + * @note Currently only supports XML format. + * @since __DEPLOY_VERSION__ + * @throws \RuntimeException on error. + */ + public function importData() + { + if ($this->from instanceof \SimpleXMLElement) + { + $xml = $this->from; + } + else + { + $xml = new \SimpleXMLElement($this->from); + } + + // Get all the table definitions. + $xmlTables = $xml->xpath('database/table_data'); + + foreach ($xmlTables as $table) + { + // Convert the magic prefix into the real table name. + $tableName = $this->getRealTableName((string) $table['name']); + + $rows = $table->children(); + + foreach ($rows as $row) + { + if ($row->getName() == 'row') + { + $entry = new \stdClass; + + foreach ($row->children() as $data) + { + $entry->{(string) $data['name']} = (string) $data; + } + + $this->db->insertObject($tableName, $entry); + } + } + } + } + /** * Merges the incoming structure definition with the existing structure. * @@ -265,9 +312,16 @@ public function mergeStructure() { // This is a new table. $sql = $this->xmlToCreate($table); + $queries = explode(';', (string) $sql); - $this->db->setQuery((string) $sql); - $this->db->execute(); + foreach ($queries as $query) + { + if (!empty($query)) + { + $this->db->setQuery((string) $query); + $this->db->execute(); + } + } } } } diff --git a/src/Mysql/MysqlDriver.php b/src/Mysql/MysqlDriver.php index 9713bafcb..89d5b9a89 100644 --- a/src/Mysql/MysqlDriver.php +++ b/src/Mysql/MysqlDriver.php @@ -473,6 +473,77 @@ public function renameTable($oldTable, $newTable, $backup = null, $prefix = null return $this; } + /** + * Inserts a row into a table based on an object's properties. + * + * @param string $table The name of the database table to insert into. + * @param object $object A reference to an object whose public properties match the table fields. + * @param string $key The name of the primary key. If provided the object property is updated. + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + * @throws \RuntimeException + */ + public function insertObject($table, &$object, $key = null) + { + $fields = []; + $values = []; + $tableColumns = $this->getTableColumns($table); + + // Iterate over the object variables to build the query fields and values. + foreach (get_object_vars($object) as $k => $v) + { + // Skip columns that don't exist in the table. + if (!array_key_exists($k, $tableColumns)) + { + continue; + } + + // Only process non-null scalars. + if (\is_array($v) || \is_object($v) || $v === null) + { + continue; + } + + // Ignore any internal fields. + if ($k[0] === '_') + { + continue; + } + + // Ignore null datetime fields. + if (($tableColumns[$k] == "datetime") && empty($v)) + { + continue; + } + + // Prepare and sanitize the fields and values for the database query. + $fields[] = $this->quoteName($k); + $values[] = $this->quote($v); + } + + // Create the base insert statement. + $query = $this->getQuery(true) + ->insert($this->quoteName($table)) + ->columns($fields) + ->values(implode(',', $values)); + + // Set the query and execute the insert. + $this->setQuery($query)->execute(); + + // Update the primary key if it exists. + $id = $this->insertid(); + + if ($key && $id && \is_string($key)) + { + $object->$key = $id; + } + + return true; + } + + /** * Method to escape a string for usage in an SQL statement. * diff --git a/src/Mysql/MysqlExporter.php b/src/Mysql/MysqlExporter.php index 9201545e1..1f8c95346 100644 --- a/src/Mysql/MysqlExporter.php +++ b/src/Mysql/MysqlExporter.php @@ -33,7 +33,15 @@ protected function buildXml() $buffer[] = ''; $buffer[] = ' '; - $buffer = array_merge($buffer, $this->buildXmlStructure()); + if ($this->options->withStructure) + { + $buffer = array_merge($buffer, $this->buildXmlStructure()); + } + + if ($this->options->withData) + { + $buffer = array_merge($buffer, $this->buildXmlData()); + } $buffer[] = ' '; $buffer[] = ''; @@ -76,6 +84,7 @@ protected function buildXmlStructure() $buffer[] = ' '; } diff --git a/src/Mysql/MysqlImporter.php b/src/Mysql/MysqlImporter.php index 40a8d0208..fb8040d55 100644 --- a/src/Mysql/MysqlImporter.php +++ b/src/Mysql/MysqlImporter.php @@ -138,6 +138,7 @@ protected function getAlterTableSql(\SimpleXMLElement $structure) && ((string) $newLookup[$name][$i]['Column_name'] === $oldLookup[$name][$i]->Column_name) && ((string) $newLookup[$name][$i]['Seq_in_index'] === $oldLookup[$name][$i]->Seq_in_index) && ((string) $newLookup[$name][$i]['Collation'] === $oldLookup[$name][$i]->Collation) + && ((string) $newLookup[$name][$i]['Sub_part'] === $oldLookup[$name][$i]->Sub_part) && ((string) $newLookup[$name][$i]['Index_type'] === $oldLookup[$name][$i]->Index_type)); /* @@ -155,6 +156,9 @@ protected function getAlterTableSql(\SimpleXMLElement $structure) echo '
Collation: '. ((string) $newLookup[$name][$i]['Collation'] == $oldLookup[$name][$i]->Collation ? 'Pass' : 'Fail').' '. (string) $newLookup[$name][$i]['Collation'].' vs '.$oldLookup[$name][$i]->Collation; + echo '
Sub_part: '. + ((string) $newLookup[$name][$i]['Sub_part'] == $oldLookup[$name][$i]->Sub_part ? 'Pass' : 'Fail').' '. + (string) $newLookup[$name][$i]['Sub_part'].' vs '.$oldLookup[$name][$i]->Sub_part; echo '
Index_type: '. ((string) $newLookup[$name][$i]['Index_type'] == $oldLookup[$name][$i]->Index_type ? 'Pass' : 'Fail').' '. (string) $newLookup[$name][$i]['Index_type'].' vs '.$oldLookup[$name][$i]->Index_type; @@ -255,7 +259,15 @@ protected function getColumnSql(\SimpleXMLElement $field) else { // TODO Don't quote numeric values. - $sql .= ' NOT NULL DEFAULT ' . $this->db->quote($fDefault); + if (strpos($fDefault, 'CURRENT') !== false) + { + $sql .= ' NOT NULL DEFAULT CURRENT_TIMESTAMP()'; + } + else + { + $sql .= ' NOT NULL DEFAULT ' . $this->db->quote($fDefault); + } + } } else @@ -356,15 +368,11 @@ protected function getKeyLookup($keys) */ protected function getKeySql($columns) { - // TODO Error checking on array and element types. - $kNonUnique = (string) $columns[0]['Non_unique']; $kName = (string) $columns[0]['Key_name']; - $kColumn = (string) $columns[0]['Column_name']; + $prefix = ''; - $prefix = ''; - - if ($kName === 'PRIMARY') + if ($kName == 'PRIMARY') { $prefix = 'PRIMARY '; } @@ -373,22 +381,21 @@ protected function getKeySql($columns) $prefix = 'UNIQUE '; } - $nColumns = \count($columns); - $kColumns = []; + $kColumns = array(); - if ($nColumns === 1) - { - $kColumns[] = $this->db->quoteName($kColumn); - } - else + foreach ($columns as $column) { - foreach ($columns as $column) + $kLength = ''; + + if (!empty($column['Sub_part'])) { - $kColumns[] = (string) $column['Column_name']; + $kLength = '(' . $column['Sub_part'] . ')'; } + + $kColumns[] = $this->db->quoteName((string) $column['Column_name']) . $kLength; } - return $prefix . 'KEY ' . ($kName !== 'PRIMARY' ? $this->db->quoteName($kName) : '') . ' (' . implode(',', $kColumns) . ')'; + return $prefix . 'KEY ' . ($kName != 'PRIMARY' ? $this->db->quoteName($kName) : '') . ' (' . implode(',', $kColumns) . ')'; } /** @@ -415,12 +422,14 @@ protected function xmlToCreate(\SimpleXMLElement $table) foreach ($table->xpath('field') as $field) { - $createTableStatement .= $this->getColumnSQL($field) . ', '; + $createTableStatement .= $this->getColumnSql($field) . ', '; } - foreach ($table->xpath('key') as $key) + $newLookup = $this->getKeyLookup($table->xpath('key')); + + foreach ($newLookup as $key) { - $createTableStatement .= $this->getKeySQL([$key]) . ', '; + $createTableStatement .= $this->getKeySql($key) . ', '; } $createTableStatement = rtrim($createTableStatement, ', '); diff --git a/src/Mysqli/MysqliDriver.php b/src/Mysqli/MysqliDriver.php index 1a03217cf..d16f9a526 100644 --- a/src/Mysqli/MysqliDriver.php +++ b/src/Mysqli/MysqliDriver.php @@ -586,6 +586,76 @@ public function insertid() return $this->connection->insert_id; } + /** + * Inserts a row into a table based on an object's properties. + * + * @param string $table The name of the database table to insert into. + * @param object $object A reference to an object whose public properties match the table fields. + * @param string $key The name of the primary key. If provided the object property is updated. + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + * @throws \RuntimeException + */ + public function insertObject($table, &$object, $key = null) + { + $fields = []; + $values = []; + $tableColumns = $this->getTableColumns($table); + + // Iterate over the object variables to build the query fields and values. + foreach (get_object_vars($object) as $k => $v) + { + // Skip columns that don't exist in the table. + if (!array_key_exists($k, $tableColumns)) + { + continue; + } + + // Only process non-null scalars. + if (\is_array($v) || \is_object($v) || $v === null) + { + continue; + } + + // Ignore any internal fields. + if ($k[0] === '_') + { + continue; + } + + // Ignore null datetime fields. + if (($tableColumns[$k] == "datetime") && empty($v)) + { + continue; + } + + // Prepare and sanitize the fields and values for the database query. + $fields[] = $this->quoteName($k); + $values[] = $this->quote($v); + } + + // Create the base insert statement. + $query = $this->getQuery(true) + ->insert($this->quoteName($table)) + ->columns($fields) + ->values(implode(',', $values)); + + // Set the query and execute the insert. + $this->setQuery($query)->execute(); + + // Update the primary key if it exists. + $id = $this->insertid(); + + if ($key && $id && \is_string($key)) + { + $object->$key = $id; + } + + return true; + } + /** * Locks a table in the database. * diff --git a/src/Mysqli/MysqliExporter.php b/src/Mysqli/MysqliExporter.php index 9814e6934..2ddcd3353 100644 --- a/src/Mysqli/MysqliExporter.php +++ b/src/Mysqli/MysqliExporter.php @@ -33,7 +33,15 @@ protected function buildXml() $buffer[] = ''; $buffer[] = ' '; - $buffer = array_merge($buffer, $this->buildXmlStructure()); + if ($this->options->withStructure) + { + $buffer = array_merge($buffer, $this->buildXmlStructure()); + } + + if ($this->options->withData) + { + $buffer = array_merge($buffer, $this->buildXmlData()); + } $buffer[] = ' '; $buffer[] = ''; @@ -76,6 +84,7 @@ protected function buildXmlStructure() $buffer[] = ' '; } diff --git a/src/Mysqli/MysqliImporter.php b/src/Mysqli/MysqliImporter.php index f7c50f030..38d7924b2 100644 --- a/src/Mysqli/MysqliImporter.php +++ b/src/Mysqli/MysqliImporter.php @@ -66,12 +66,14 @@ protected function xmlToCreate(\SimpleXMLElement $table) foreach ($table->xpath('field') as $field) { - $createTableStatement .= $this->getColumnSQL($field) . ', '; + $createTableStatement .= $this->getColumnSql($field) . ', '; } - foreach ($table->xpath('key') as $key) + $newLookup = $this->getKeyLookup($table->xpath('key')); + + foreach ($newLookup as $key) { - $createTableStatement .= $this->getKeySQL([$key]) . ', '; + $createTableStatement .= $this->getKeySql($key) . ', '; } $createTableStatement = rtrim($createTableStatement, ', '); @@ -176,6 +178,7 @@ protected function getAlterTableSql(\SimpleXMLElement $structure) && ((string) $newLookup[$name][$i]['Column_name'] === $oldLookup[$name][$i]->Column_name) && ((string) $newLookup[$name][$i]['Seq_in_index'] === $oldLookup[$name][$i]->Seq_in_index) && ((string) $newLookup[$name][$i]['Collation'] === $oldLookup[$name][$i]->Collation) + && ((string) $newLookup[$name][$i]['Sub_part'] == $oldLookup[$name][$i]->Sub_part) && ((string) $newLookup[$name][$i]['Index_type'] === $oldLookup[$name][$i]->Index_type)); /* @@ -193,6 +196,9 @@ protected function getAlterTableSql(\SimpleXMLElement $structure) echo '
Collation: '. ((string) $newLookup[$name][$i]['Collation'] == $oldLookup[$name][$i]->Collation ? 'Pass' : 'Fail').' '. (string) $newLookup[$name][$i]['Collation'].' vs '.$oldLookup[$name][$i]->Collation; + echo '
Sub_part: '. + ((string) $newLookup[$name][$i]['Sub_part'] == $oldLookup[$name][$i]->Sub_part ? 'Pass' : 'Fail').' '. + (string) $newLookup[$name][$i]['Sub_part'].' vs '.$oldLookup[$name][$i]->Sub_part; echo '
Index_type: '. ((string) $newLookup[$name][$i]['Index_type'] == $oldLookup[$name][$i]->Index_type ? 'Pass' : 'Fail').' '. (string) $newLookup[$name][$i]['Index_type'].' vs '.$oldLookup[$name][$i]->Index_type; @@ -292,7 +298,14 @@ protected function getColumnSql(\SimpleXMLElement $field) else { // TODO Don't quote numeric values. - $sql .= ' NOT NULL DEFAULT ' . $this->db->quote($fDefault); + if (strpos($fDefault, 'CURRENT') !== false) + { + $sql .= ' NOT NULL DEFAULT CURRENT_TIMESTAMP()'; + } + else + { + $sql .= ' NOT NULL DEFAULT ' . $this->db->quote($fDefault); + } } } else @@ -392,15 +405,11 @@ protected function getKeyLookup($keys) */ protected function getKeySql($columns) { - // TODO Error checking on array and element types. - $kNonUnique = (string) $columns[0]['Non_unique']; $kName = (string) $columns[0]['Key_name']; - $kColumn = (string) $columns[0]['Column_name']; + $prefix = ''; - $prefix = ''; - - if ($kName === 'PRIMARY') + if ($kName == 'PRIMARY') { $prefix = 'PRIMARY '; } @@ -409,21 +418,20 @@ protected function getKeySql($columns) $prefix = 'UNIQUE '; } - $nColumns = \count($columns); - $kColumns = []; + $kColumns = array(); - if ($nColumns === 1) - { - $kColumns[] = $this->db->quoteName($kColumn); - } - else + foreach ($columns as $column) { - foreach ($columns as $column) + $kLength = ''; + + if (!empty($column['Sub_part'])) { - $kColumns[] = (string) $column['Column_name']; + $kLength = '(' . $column['Sub_part'] . ')'; } + + $kColumns[] = $this->db->quoteName((string) $column['Column_name']) . $kLength; } - return $prefix . 'KEY ' . ($kName !== 'PRIMARY' ? $this->db->quoteName($kName) : '') . ' (' . implode(',', $kColumns) . ')'; + return $prefix . 'KEY ' . ($kName != 'PRIMARY' ? $this->db->quoteName($kName) : '') . ' (' . implode(',', $kColumns) . ')'; } } diff --git a/src/Pgsql/PgsqlDriver.php b/src/Pgsql/PgsqlDriver.php index c80cf2683..d1be2d6eb 100644 --- a/src/Pgsql/PgsqlDriver.php +++ b/src/Pgsql/PgsqlDriver.php @@ -226,16 +226,6 @@ public function getTableColumns($table, $typeOnly = true) { foreach ($fields as $field) { - if (stristr(strtolower($field->type), 'character varying')) - { - $field->Default = ''; - } - - if (stristr(strtolower($field->type), 'text')) - { - $field->Default = ''; - } - // Do some dirty translation to MySQL output. // @todo: Come up with and implement a standard across databases. $result[$field->column_name] = (object) [ @@ -281,12 +271,13 @@ public function getTableKeys($table) // To check if table exists and prevent SQL injection $tableList = $this->getTableList(); + $tableSub = $this->replacePrefix($table); - if (\in_array($table, $tableList, true)) + if (\in_array($tableSub, $tableList, true)) { // Get the details columns information. $this->setQuery(' - SELECT indexname AS "idxName", indisprimary AS "isPrimary", indisunique AS "isUnique", + SELECT indexname AS "idxName", indisprimary AS "isPrimary", indisunique AS "isUnique", indkey AS "indKey", CASE WHEN indisprimary = true THEN ( SELECT \'ALTER TABLE \' || tablename || \' ADD \' || pg_catalog.pg_get_constraintdef(const.oid, true) FROM pg_constraint AS const WHERE const.conname= pgClassFirst.relname ) @@ -295,7 +286,7 @@ public function getTableKeys($table) FROM pg_indexes LEFT JOIN pg_class AS pgClassFirst ON indexname=pgClassFirst.relname LEFT JOIN pg_index AS pgIndex ON pgClassFirst.oid=pgIndex.indexrelid - WHERE tablename=' . $this->quote($table) . ' ORDER BY indkey' + WHERE tablename=' . $this->quote($tableSub) . ' ORDER BY indkey' ); return $this->loadObjectList(); @@ -304,6 +295,40 @@ public function getTableKeys($table) return []; } + /** + * Get the list of column names this index indexes. + * + * @param string $table The name of the table. + * @param string $indKey The list of column numbers for the table + * + * @return string A list of the column names for the table. + * + * @since __DEPLOY_VERSION__ + * @throws \RuntimeException + */ + public function getNamesKey($table, $indKey) + { + $this->connect(); + + $tableSub = $this->replacePrefix($table); + + $tabInd = explode(' ', $indKey); + $colNames = array(); + + foreach ($tabInd as $numCol) + { + $query = $this->getQuery(true) + ->select('attname') + ->from('pg_attribute') + ->join('LEFT', 'pg_class ON pg_class.relname=' . $this->quote($tableSub)) + ->where('attnum=' . $numCol . ' AND attrelid=pg_class.oid'); + $this->setQuery($query); + $colNames[] = $this->loadResult(); + } + + return implode(', ', $colNames); + } + /** * Method to get an array of all tables in the database. * @@ -340,8 +365,9 @@ public function getTableSequences($table) { // To check if table exists and prevent SQL injection $tableList = $this->getTableList(); + $tableSub = $this->replacePrefix($table); - if (\in_array($table, $tableList, true)) + if (\in_array($tableSub, $tableList, true)) { $name = [ 's.relname', 'n.nspname', 't.relname', 'a.attname', 'info.data_type', @@ -361,7 +387,7 @@ public function getTableSequences($table) ->leftJoin('pg_namespace n ON n.oid = t.relnamespace') ->leftJoin('pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.refobjsubid') ->leftJoin('information_schema.sequences AS info ON info.sequence_name = s.relname') - ->where('s.relkind = ' . $this->quote('S') . ' AND d.deptype = ' . $this->quote('a') . ' AND t.relname = ' . $this->quote($table)); + ->where('s.relkind = ' . $this->quote('S') . ' AND d.deptype = ' . $this->quote('a') . ' AND t.relname = ' . $this->quote($tableSub)); $this->setQuery($query); return $this->loadObjectList(); @@ -370,6 +396,52 @@ public function getTableSequences($table) return []; } + /** + * Method to get the last value of a sequence in the database. + * + * @param string $sequence The name of the sequence. + * + * @return integer The last value of the sequence. + * + * @since __DEPLOY_VERSION__ + * @throws \RuntimeException + */ + public function getSequenceLastValue($sequence) + { + $this->connect(); + + $query = $this->getQuery(true) + ->select($this->quoteName('last_value')) + ->from($sequence); + + $this->setQuery($query); + + return $this->loadResult(); + } + + /** + * Method to get the is_called attribute of a sequence. + * + * @param string $sequence The name of the sequence. + * + * @return boolean The is_called attribute of the sequence. + * + * @since __DEPLOY_VERSION__ + * @throws \RuntimeException + */ + public function getSequenceIsCalled($sequence) + { + $this->connect(); + + $query = $this->getQuery(true) + ->select($this->quoteName('is_called')) + ->from($sequence); + + $this->setQuery($query); + + return $this->loadResult(); + } + /** * Locks a table in the database. * @@ -510,7 +582,7 @@ public function sqlValue($columns, $field_name, $field_value) break; case 'date': - case 'timestamp without time zone': + //case 'timestamp without time zone': if (empty($field_value)) { $field_value = $this->getNullDate(); @@ -654,6 +726,12 @@ public function insertObject($table, &$object, $key = null) continue; } + // Ignore null timestamp fields. + if (($columns[$k] == "timestamp without time zone") && empty($v)) + { + continue; + } + // Prepare and sanitize the fields and values for the database query. $fields[] = $this->quoteName($k); $values[] = $this->sqlValue($columns, $k, $v); diff --git a/src/Pgsql/PgsqlExporter.php b/src/Pgsql/PgsqlExporter.php index 5c6a12242..127cc5a99 100644 --- a/src/Pgsql/PgsqlExporter.php +++ b/src/Pgsql/PgsqlExporter.php @@ -33,7 +33,15 @@ protected function buildXml() $buffer[] = ''; $buffer[] = ' '; - $buffer = array_merge($buffer, $this->buildXmlStructure()); + if ($this->options->withStructure) + { + $buffer = array_merge($buffer, $this->buildXmlStructure()); + } + + if ($this->options->withData) + { + $buffer = array_merge($buffer, $this->buildXmlData()); + } $buffer[] = ' '; $buffer[] = ''; @@ -67,24 +75,26 @@ protected function buildXmlStructure() foreach ($sequences as $sequence) { - $buffer[] = ' getGenericTableName($sequence->sequence) . '"' . ' Schema="' . $sequence->schema . '"' . + ' Table="' . $table . '"' . ' Column="' . $sequence->column . '"' . ' Type="' . $sequence->data_type . '"' . ' Start_Value="' . $sequence->start_value . '" Min_Value="' . $sequence->minimum_value . '"' . - ' Max_Value="' . $sequence->maximum_value . '" Increment="' . $sequence->increment . '"' . - ' Cycle_option="' . $sequence->cycle_option . '"' . + ' Max_Value="' . $sequence->maximum_value . '"' . ' Last_Value="' . $this->db->getSequenceLastValue($sequence->sequence) . '"' . + ' Increment="' . $sequence->increment . '"' . ' Cycle_option="' . $sequence->cycle_option . '"' . + ' Is_called="' . $this->db->getSequenceIsCalled($sequence->sequence) . '"' . ' />'; } foreach ($fields as $field) { $buffer[] = ' default) ? ' Default="' . $field->default . '"' : '') . ' Comments="' . $field->comments . '" />'; + ' Default="' . $field->Default . '"' . ' Comments="' . $field->comments . '" />'; } foreach ($keys as $key) { - $buffer[] = ' '; + $buffer[] = ' Query . '\' />'; } $buffer[] = '
'; @@ -93,6 +103,76 @@ protected function buildXmlStructure() return $buffer; } + /** + * Builds the XML data to export. + * + * @return array An array of XML lines (strings). + * + * @since __DEPLOY_VERSION__ + * @throws \Exception if an error occurs. + */ + protected function buildXmlData() + { + $buffer = array(); + + foreach ($this->from as $table) + { + // Replace the magic prefix if found. + $table = $this->getGenericTableName($table); + + // Get the details columns information. + $fields = $this->db->getTableColumns($table, false); + $colblob = array(); + + foreach ($fields as $field) + { + // Catch blob for xml conversion + // PostgreSQL binary large object type + if ($field->Type == 'bytea') + { + $colblob[] = $field->Field; + } + } + + $query = $this->db->getQuery(true); + $query->select($query->quoteName(array_keys($fields))) + ->from($query->quoteName($table)); + $this->db->setQuery($query); + + $rows = $this->db->loadObjectList(); + + if (!count($rows)) + { + continue; + } + + $buffer[] = ' '; + + foreach ($rows as $row) + { + $buffer[] = ' '; + + foreach ($row as $key => $value) + { + if (!in_array($key, $colblob)) + { + $buffer[] = ' ' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . ''; + } + else + { + $buffer[] = ' ' . stream_get_contents($value) . ''; + } + } + + $buffer[] = ' '; + } + + $buffer[] = ' '; + } + + return $buffer; + } + /** * Checks if all data and options are in order prior to exporting. * diff --git a/src/Pgsql/PgsqlImporter.php b/src/Pgsql/PgsqlImporter.php index f4617e7b0..ca5af0e1b 100644 --- a/src/Pgsql/PgsqlImporter.php +++ b/src/Pgsql/PgsqlImporter.php @@ -107,6 +107,7 @@ protected function getAlterTableSql(\SimpleXMLElement $structure) if ($change) { $alters[] = $this->getChangeSequenceSql($kSeqName, $vSeq); + $alters[] = $this->getSetvalSequenceSql($kSeqName, $vSeq); } // Unset this field so that what we have left are fields that need to be removed. @@ -116,6 +117,7 @@ protected function getAlterTableSql(\SimpleXMLElement $structure) { // The sequence is new $alters[] = $this->getAddSequenceSql($newSequenceLook[$kSeqName][0]); + $alters[] = $this->getSetvalSequenceSql($newSequenceLook[$kSeqName][0]); } } @@ -262,7 +264,7 @@ protected function getDropSequenceSql($name) */ protected function getAddSequenceSql(\SimpleXMLElement $field) { - $sql = 'CREATE SEQUENCE ' . (string) $field['Name'] + $sql = 'CREATE SEQUENCE IF NOT EXISTS ' . (string) $field['Name'] . ' INCREMENT BY ' . (string) $field['Increment'] . ' MINVALUE ' . $field['Min_Value'] . ' MAXVALUE ' . (string) $field['Max_Value'] . ' START ' . (string) $field['Start_Value'] . (((string) $field['Cycle_option'] === 'NO') ? ' NO' : '') . ' CYCLE' @@ -290,6 +292,22 @@ protected function getChangeSequenceSql(\SimpleXMLElement $field) return $sql; } + /** + * Get the syntax to setval a sequence. + * + * @param \SimpleXMLElement $field The XML definition for the sequence. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + protected function getSetvalSequenceSql($field) + { + $is_called = $field['Is_called'] == 't' || $field['Is_called'] == '1' ? 'TRUE' : 'FALSE'; + + return 'SELECT setval(\'' . (string) $field['Name'] . '\', ' . (string) $field['Last_Value'] . ', ' . $is_called . ')'; + } + /** * Get the syntax to alter a column. * @@ -376,18 +394,22 @@ protected function getAlterColumnSql($table, \SimpleXMLElement $field) */ protected function getColumnSql(\SimpleXMLElement $field) { - // TODO Incorporate into parent class and use $this. - $blobs = ['text', 'smalltext', 'mediumtext', 'largetext']; - $fName = (string) $field['Field']; $fType = (string) $field['Type']; $fNull = (string) $field['Null']; - $fDefault = (isset($field['Default']) && $field['Default'] != 'NULL') ? - preg_match('/^[0-9]$/', $field['Default']) ? $field['Default'] : $this->db->quote((string) $field['Default']) - : null; + if (strpos($field['Default'], '::') != false) + { + $fDefault = strstr($field['Default'], '::', true); + } + else + { + $fDefault = isset($field['Default']) && strlen($field['Default']) != 0 ? + preg_match('/^[0-9]$/', $field['Default']) ? $field['Default'] : $this->db->quote((string) $field['Default']) + : null; + } - // Note, nextval() as default value means that type field is serial. + /* nextval() as default value means that type field is serial */ if (strpos($fDefault, 'nextval') !== false) { $sql = $this->db->quoteName($fName) . ' SERIAL'; @@ -396,9 +418,9 @@ protected function getColumnSql(\SimpleXMLElement $field) { $sql = $this->db->quoteName($fName) . ' ' . $fType; - if ($fNull === 'NO') + if ($fNull == 'NO') { - if ($fDefault === null || \in_array($fType, $blobs, true)) + if ($fDefault === null) { $sql .= ' NOT NULL'; } @@ -484,6 +506,34 @@ protected function getKeyLookup($keys) return $lookup; } + /** + * Get the SQL syntax to add a unique constraint for a table key. + * + * @param string $table The table name. + * @param array $key The key. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + protected function getAddUniqueSql($table, $key) + { + if ($key instanceof \SimpleXMLElement) + { + $kName = (string) $key['Key_name']; + $kIndex = (string) $key['Index']; + } + else + { + $kName = $key->Key_name; + $kIndex = $key->Index; + } + + $unique = $kIndex . ' UNIQUE (' . $kName . ')'; + + return 'ALTER TABLE ' . $this->db->quoteName($table) . ' ADD CONSTRAINT ' . $unique; + } + /** * Get the details list of sequences for a table. * @@ -532,7 +582,42 @@ protected function getSeqLookup($sequences) */ protected function xmlToCreate(\SimpleXMLElement $table) { - // TODO - Implement this - return ''; + $existingTables = $this->db->getTableList(); + $tableName = (string) $table['name']; + + if (in_array($tableName, $existingTables)) + { + throw new \RuntimeException('The table you are trying to create already exists'); + } + + $createTableStatement = 'CREATE TABLE ' . $this->db->quoteName($tableName) . ' ('; + + foreach ($table->xpath('field') as $field) + { + $createTableStatement .= $this->getColumnSql($field) . ', '; + } + + $createTableStatement = rtrim($createTableStatement, ', '); + $createTableStatement .= ');'; + + foreach ($table->xpath('sequence') as $seq) + { + $createTableStatement .= $this->getAddSequenceSql($seq) . ';'; + $createTableStatement .= $this->getSetvalSequenceSql($seq) . ';'; + } + + foreach ($table->xpath('key') as $key) + { + if ((($key['is_primary'] == 'f') || ($key['is_primary'] == '')) && (($key['is_unique'] == 't') || ($key['is_unique'] == '1'))) + { + $createTableStatement .= $this->getAddUniqueSql($tableName, $key) . ';'; + } + else + { + $createTableStatement .= $this->getAddIndexSql($key) . ';'; + } + } + + return $createTableStatement; } }