diff --git a/cli/exporter.php b/cli/exporter.php new file mode 100644 index 0000000000000..5fde761660bdf --- /dev/null +++ b/cli/exporter.php @@ -0,0 +1,142 @@ +load('files_joomla.sys', JPATH_SITE, null, false, false) +// Fallback to the files_joomla file in the default language +|| $lang->load('files_joomla.sys', JPATH_SITE, null, true); + +/** + * A command line cron job to export tables and data. + * + * @since __DEPLOY_VERSION__ + */ +class DbExporterCli extends JApplicationCli +{ + /** + * Entry point for CLI script + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function doExecute() + { + $this->out(JText::_('DbExporterCli')); + $this->out('============================'); + $total_time = microtime(true); + + // Import the dependencies + jimport('joomla.filesystem.file'); + + $tables = JFactory::getDbo()->getTableList(); + $prefix = JFactory::getDbo()->getPrefix(); + $exp = JFactory::getDbo()->getExporter()->withStructure(); + + $iall = $this->input->getString('all', null); + $ihelp = $this->input->getString('help', null); + $itable = $this->input->getString('table', null); + $imode = $this->input->getString('mode', null); + $ipath = $this->input->get('folder', null, 'folder'); + $zipfile = $ipath . 'jdata_exported_' . JFactory::getDate()->format('Y-m-d') . '.zip'; + + if (!($itable || $iall || $ihelp)) + { + if (!$ihelp) + { + $this->out('[WARNING] Missing or wrong parameters'); + $this->out(); + } + + $this->out('Usage: php exporter.php '); + $this->out('php exporter.php --all dump all tables'); + $this->out('php exporter.php --mode zip dump in zip format'); + $this->out('php exporter.php --table dump '); + $this->out('php exporter.php --folder dump in '); + + return; + } + + if ($itable) + { + if (!in_array($itable, $tables)) + { + $this->out('Not Found ' . $itable . '....'); + $this->out(); + + return; + } + + $tables = array($itable); + } + + $zip = JArchive::getAdapter('zip'); + + foreach ($tables as $table) + { + if (strpos(substr($table, 0, strlen($prefix)), $prefix) !== false) + { + $task_i_time = microtime(true); + $filename = $ipath . '/' . $table . '.xml'; + $this->out(); + $this->out('Exporting ' . $table . '....'); + $data = (string) $exp->from($table)->withData(true); + + if (JFile::exists($filename)) + { + JFile::delete($filename); + } + + JFile::write($filename, $data); + + if ($imode && $imode === 'zip') + { + $zipFilesArray[] = array('name' => $table . '.xml', 'data' => $data); + $zip->create($zipfile, $zipFilesArray); + JFile::delete($filename); + } + + $this->out('Exported in ' . round(microtime(true) - $task_i_time, 3)); + } + } + + $this->out('Total time: ' . round(microtime(true) - $total_time, 3)); + } +} + +// Instantiate the application object, passing the class name to JCli::getInstance +// and use chaining to execute the application. +JApplicationCli::getInstance('DbExporterCli')->execute(); diff --git a/cli/importer.php b/cli/importer.php new file mode 100644 index 0000000000000..e2e2a4b9b82eb --- /dev/null +++ b/cli/importer.php @@ -0,0 +1,170 @@ +load('files_joomla.sys', JPATH_SITE, null, false, false) +// Fallback to the files_joomla file in the default language +|| $lang->load('files_joomla.sys', JPATH_SITE, null, true); + +/** + * A command line cron job to import tables and data. + * + * @since __DEPLOY_VERSION__ + */ +class DbImporterCli extends JApplicationCli +{ + /** + * Entry point for CLI script + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function doExecute() + { + $this->out(JText::_('DbImporterCli')); + $this->out('============================'); + $total_time = microtime(true); + + // Import the dependencies + jimport('joomla.filesystem.file'); + jimport('joomla.filesystem.folder'); + + $ipath = $this->input->get('folder', null, 'folder'); + $iall = $this->input->getString('all', null); + $ihelp = $this->input->getString('help', null); + $itable = $this->input->getString('table', null); + $tables = JFolder::files($ipath, '\.xml$'); + + if (!($itable || $iall || $ihelp)) + { + if (!$ihelp) + { + $this->out('[WARNING] Missing or wrong parameters'); + $this->out(); + } + + $this->out('Usage: php importer.php '); + $this->out('php importer.php --all import all files'); + $this->out('php importer.php --table import '); + $this->out('php importer.php --folder import from '); + + return; + } + + if ($this->input->getString('table', false)) + { + $tables = array($itable . '.xml'); + } + + $db = JFactory::getDbo(); + $prefix = $db->getPrefix(); + + foreach ($tables as $table) + { + $task_i_time = microtime(true); + $percorso = $ipath . '/' . $table; + + // Check file + if (!JFile::exists($percorso)) + { + $this->out('Not Found ' . $table); + + return false; + } + + $table_name = str_replace('.xml', '', $table); + $this->out('Importing ' . $table_name . ' from ' . $table); + + try + { + $imp = JFactory::getDbo()->getImporter()->from(JFile::read($percorso))->withStructure()->asXml(); + } + catch (JDatabaseExceptionExecuting $e) + { + $this->out('Error on getImporter' . $table . ' ' . $e); + + return false; + } + + $this->out('Reading data from ' . $table); + + try + { + $this->out('Drop ' . $table_name); + $db->dropTable($table_name, true); + } + catch (JDatabaseExceptionExecuting $e) + { + $this->out(' Error in DROP TABLE ' . $table_name . ' ' . $e); + + return false; + } + + try + { + $imp->mergeStructure(); + } + catch (JDatabaseExceptionExecuting $e) + { + $this->out('Error on mergeStructure' . $table . ' ' . $e); + + return false; + } + + $this->out('Checked structure ' . $table); + + try + { + $imp->importData(); + } + catch (JDatabaseExceptionExecuting $e) + { + $this->out('Error on importData' . $table . ' ' . $e); + + return false; + } + $this->out('Data loaded ' . $table . ' in ' . round(microtime(true) - $task_i_time, 3)); + $this->out(); + } + + $this->out('Total time: ' . round(microtime(true) - $total_time, 3)); + } +} + +// Instantiate the application object, passing the class name to JCli::getInstance +// and use chaining to execute the application. +JApplicationCli::getInstance('DbImporterCli')->execute(); diff --git a/libraries/joomla/database/driver/pgsql.php b/libraries/joomla/database/driver/pgsql.php index a1552800f680d..48382501af6c4 100644 --- a/libraries/joomla/database/driver/pgsql.php +++ b/libraries/joomla/database/driver/pgsql.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) array( @@ -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 false; } + /** + * 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 = array( 's.relname', 'n.nspname', 't.relname', 'a.attname', 'info.data_type', @@ -365,7 +391,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(); @@ -374,6 +400,52 @@ public function getTableSequences($table) return false; } + /** + * 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. * diff --git a/libraries/joomla/database/driver/postgresql.php b/libraries/joomla/database/driver/postgresql.php index 368dee2e08283..3b10f0bfc8bdf 100644 --- a/libraries/joomla/database/driver/postgresql.php +++ b/libraries/joomla/database/driver/postgresql.php @@ -446,15 +446,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) array( @@ -500,12 +491,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)) + if (in_array($tableSub, $tableList)) { // 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 ) @@ -514,17 +506,50 @@ 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' ); $keys = $this->loadObjectList(); - return $keys; } return false; } + /** + * 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. * @@ -566,8 +591,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)) + if (in_array($tableSub, $tableList)) { $name = array( 's.relname', 'n.nspname', 't.relname', 'a.attname', 'info.data_type', 'info.minimum_value', 'info.maximum_value', @@ -590,7 +616,7 @@ public function getTableSequences($table) ->join('LEFT', 'pg_namespace n ON n.oid=t.relnamespace') ->join('LEFT', 'pg_attribute a ON a.attrelid=t.oid AND a.attnum=d.refobjsubid') ->join('LEFT', 'information_schema.sequences AS info ON info.sequence_name=s.relname') - ->where("s.relkind='S' AND d.deptype='a' AND t.relname=" . $this->quote($table)); + ->where("s.relkind='S' AND d.deptype='a' AND t.relname=" . $this->quote($tableSub)); $this->setQuery($query); $seq = $this->loadObjectList(); @@ -600,6 +626,52 @@ public function getTableSequences($table) return false; } + /** + * 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(); + } + + /** + * 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(); + } + /** * Get the version of the database connector. * diff --git a/libraries/joomla/database/exporter.php b/libraries/joomla/database/exporter.php index 6f1f82d80dd95..0adfbb4a11578 100644 --- a/libraries/joomla/database/exporter.php +++ b/libraries/joomla/database/exporter.php @@ -71,8 +71,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(); @@ -227,4 +228,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 JDatabaseExporter Method supports chaining. + * + * @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/libraries/joomla/database/exporter/mysqli.php b/libraries/joomla/database/exporter/mysqli.php index a3c5a56236958..d73c081e9d1f0 100644 --- a/libraries/joomla/database/exporter/mysqli.php +++ b/libraries/joomla/database/exporter/mysqli.php @@ -32,7 +32,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[] = ''; @@ -75,6 +83,7 @@ protected function buildXmlStructure() $buffer[] = ' '; } diff --git a/libraries/joomla/database/exporter/pdomysql.php b/libraries/joomla/database/exporter/pdomysql.php index b902a213e5ed4..3f91aeb065912 100644 --- a/libraries/joomla/database/exporter/pdomysql.php +++ b/libraries/joomla/database/exporter/pdomysql.php @@ -34,7 +34,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[] = ''; @@ -77,6 +85,7 @@ protected function buildXmlStructure() $buffer[] = ' '; } diff --git a/libraries/joomla/database/exporter/pgsql.php b/libraries/joomla/database/exporter/pgsql.php index 5a67836957478..0d9608be23499 100644 --- a/libraries/joomla/database/exporter/pgsql.php +++ b/libraries/joomla/database/exporter/pgsql.php @@ -40,4 +40,74 @@ public function check() 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) + { + // 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; + } } diff --git a/libraries/joomla/database/exporter/postgresql.php b/libraries/joomla/database/exporter/postgresql.php index 94dc9c8f36a70..36d7b0de9fdbb 100644 --- a/libraries/joomla/database/exporter/postgresql.php +++ b/libraries/joomla/database/exporter/postgresql.php @@ -35,7 +35,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[] = ''; @@ -57,7 +65,6 @@ protected function buildXmlStructure() foreach ($this->from as $table) { - // Replace the magic prefix if found. $table = $this->getGenericTableName($table); // Get the details columns information. @@ -67,32 +74,40 @@ protected function buildXmlStructure() $buffer[] = ' '; - foreach ($sequences as $sequence) + if ($sequences) { - if (version_compare($this->db->getVersion(), '9.1.0') < 0) + foreach ($sequences as $sequence) { - $sequence->start_value = null; + if (version_compare($this->db->getVersion(), '9.1.0') < 0) + { + $sequence->start_value = null; + } + + $buffer[] = ' '; } - - $buffer[] = ' '; } foreach ($fields as $field) { $buffer[] = ' default) ? ' Default="' . $field->default . '"' : '') . ' Comments="' . $field->comments . '"' . + ' Default="' . $field->Default . '"' . ' Comments="' . $field->comments . '"' . ' />'; } - foreach ($keys as $key) + if ($keys) { - $buffer[] = ' '; + foreach ($keys as $key) + { + $buffer[] = ' Query . '\' />'; + } } $buffer[] = ' '; @@ -101,6 +116,75 @@ 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) + { + $table = $this->getGenericTableName($table); + + // Get the details columns information. + $fields = $this->db->getTableColumns($table, false); + $colblob = array(); + + foreach ($fields as $field) + { + // Cacth 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[] = ' ' . pg_unescape_bytea($value) . ''; + } + } + + $buffer[] = ' '; + } + + $buffer[] = ' '; + } + + return $buffer; + } + /** * Checks if all data and options are in order prior to exporting. * diff --git a/libraries/joomla/database/exporter/sqlsrv.php b/libraries/joomla/database/exporter/sqlsrv.php new file mode 100644 index 0000000000000..b4bf4945dd99b --- /dev/null +++ b/libraries/joomla/database/exporter/sqlsrv.php @@ -0,0 +1,121 @@ +'; + $buffer[] = ''; + $buffer[] = ' '; + + if ($this->options->withStructure) + { + $buffer = array_merge($buffer, $this->buildXmlStructure()); + } + + if ($this->options->withData) + { + $buffer = array_merge($buffer, $this->buildXmlData()); + } + + $buffer[] = ' '; + $buffer[] = ''; + + return implode("\n", $buffer); + } + + /** + * Builds the XML structure to export. + * + * @return array An array of XML lines (strings). + * + * @since __DEPLOY_VERSION__ + * @throws Exception if an error occurs. + */ + protected function buildXmlStructure() + { + $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); + $keys = $this->db->getTableKeys($table); + + $buffer[] = ' '; + + foreach ($fields as $field) + { + $buffer[] = ' Key) ? ' Key="' . $field->Key . '"' : '') . + (isset($field->Default) ? ' Default="' . $field->Default . '"' : '') . + ' />'; + } + + foreach ($keys as $key) + { + $buffer[] = ' '; + } + + $buffer[] = ' '; + } + + return $buffer; + } + + /** + * Checks if all data and options are in order prior to exporting. + * + * @return JDatabaseExporterSqlsrv Method supports chaining. + * + * @since __DEPLOY_VERSION__ + * @throws Exception if an error is encountered. + */ + public function check() + { + // Check if the db connector has been set. + if (!($this->db instanceof JDatabaseDriverSqlsrv)) + { + throw new Exception('JPLATFORM_ERROR_DATABASE_CONNECTOR_WRONG_TYPE'); + } + + // Check if the tables have been specified. + if (empty($this->from)) + { + throw new Exception('JPLATFORM_ERROR_NO_TABLES_SPECIFIED'); + } + + return $this; + } +} diff --git a/libraries/joomla/database/importer.php b/libraries/joomla/database/importer.php index edf9b5861b771..e1fd65cc7e7c2 100644 --- a/libraries/joomla/database/importer.php +++ b/libraries/joomla/database/importer.php @@ -152,6 +152,51 @@ 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) + { + $tableName = (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. * @@ -201,9 +246,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($query); + $this->db->execute(); + } + } } } } diff --git a/libraries/joomla/database/importer/mysqli.php b/libraries/joomla/database/importer/mysqli.php index e98dc65639967..645af7254c624 100644 --- a/libraries/joomla/database/importer/mysqli.php +++ b/libraries/joomla/database/importer/mysqli.php @@ -194,6 +194,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)); /* @@ -310,7 +311,14 @@ protected function getColumnSql(SimpleXMLElement $field) else { // TODO Don't quote numeric values. - $query .= ' NOT NULL DEFAULT ' . $this->db->quote($fDefault); + if (strpos($fDefault, 'CURRENT') !== false) + { + $query .= ' NOT NULL DEFAULT CURRENT_TIMESTAMP()'; + } + else + { + $query .= ' NOT NULL DEFAULT ' . $this->db->quote($fDefault); + } } } else @@ -411,13 +419,9 @@ 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 = ''; + $kName = (string) $columns[0]['Key_name']; + $prefix = ''; if ($kName == 'PRIMARY') { @@ -428,23 +432,20 @@ protected function getKeySql($columns) $prefix = 'UNIQUE '; } - $nColumns = count($columns); $kColumns = array(); - if ($nColumns == 1) + foreach ($columns as $column) { - $kColumns[] = $this->db->quoteName($kColumn); - } - else - { - foreach ($columns as $column) + $kLength = ''; + + if (!empty($column['Sub_part'])) { - $kColumns[] = (string) $column['Column_name']; + $kLength = '(' . $column['Sub_part'] . ')'; } - } - $query = $prefix . 'KEY ' . ($kName != 'PRIMARY' ? $this->db->quoteName($kName) : '') . ' (' . implode(',', $kColumns) . ')'; + $kColumns[] = $this->db->quoteName((string) $column['Column_name']) . $kLength; + } - return $query; + return $prefix . 'KEY ' . ($kName != 'PRIMARY' ? $this->db->quoteName($kName) : '') . ' (' . implode(',', $kColumns) . ')'; } } diff --git a/libraries/joomla/database/importer/pdomysql.php b/libraries/joomla/database/importer/pdomysql.php index 18c00fde2b691..8583b9f1920c5 100644 --- a/libraries/joomla/database/importer/pdomysql.php +++ b/libraries/joomla/database/importer/pdomysql.php @@ -30,9 +30,7 @@ class JDatabaseImporterPdomysql extends JDatabaseImporter */ protected function getAddColumnSql($table, SimpleXMLElement $field) { - $sql = 'ALTER TABLE ' . $this->db->quoteName($table) . ' ADD COLUMN ' . $this->getColumnSql($field); - - return $sql; + return 'ALTER TABLE ' . $this->db->quoteName($table) . ' ADD COLUMN ' . $this->getColumnSql($field); } /** @@ -47,9 +45,7 @@ protected function getAddColumnSql($table, SimpleXMLElement $field) */ protected function getAddKeySql($table, $keys) { - $sql = 'ALTER TABLE ' . $this->db->quoteName($table) . ' ADD ' . $this->getKeySql($keys); - - return $sql; + return 'ALTER TABLE ' . $this->db->quoteName($table) . ' ADD ' . $this->getKeySql($keys); } /** @@ -133,6 +129,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)); /* @@ -214,10 +211,8 @@ protected function getAlterTableSql(SimpleXMLElement $structure) */ protected function getChangeColumnSql($table, SimpleXMLElement $field) { - $sql = 'ALTER TABLE ' . $this->db->quoteName($table) . ' CHANGE COLUMN ' . $this->db->quoteName((string) $field['Field']) . ' ' + return 'ALTER TABLE ' . $this->db->quoteName($table) . ' CHANGE COLUMN ' . $this->db->quoteName((string) $field['Field']) . ' ' . $this->getColumnSql($field); - - return $sql; } /** @@ -241,39 +236,46 @@ protected function getColumnSql(SimpleXMLElement $field) $fDefault = isset($field['Default']) ? (string) $field['Default'] : null; $fExtra = (string) $field['Extra']; - $sql = $this->db->quoteName($fName) . ' ' . $fType; + $query = $this->db->quoteName($fName) . ' ' . $fType; if ($fNull == 'NO') { if (in_array($fType, $blobs) || $fDefault === null) { - $sql .= ' NOT NULL'; + $query .= ' NOT NULL'; } else { // TODO Don't quote numeric values. - $sql .= ' NOT NULL DEFAULT ' . $this->db->quote($fDefault); + if (strpos($fDefault, 'CURRENT') !== false) + { + $query .= ' NOT NULL DEFAULT CURRENT_TIMESTAMP()'; + } + else + { + $query .= ' NOT NULL DEFAULT ' . $this->db->quote($fDefault); + } } } else { if ($fDefault === null) { - $sql .= ' DEFAULT NULL'; + $query .= ' DEFAULT NULL'; } else { // TODO Don't quote numeric values. - $sql .= ' DEFAULT ' . $this->db->quote($fDefault); + $query .= ' DEFAULT ' . $this->db->quote($fDefault); } } if ($fExtra) { - $sql .= ' ' . strtoupper($fExtra); + $query .= ' ' . strtoupper($fExtra); } - return $sql; + return $query; } /** @@ -288,9 +290,7 @@ protected function getColumnSql(SimpleXMLElement $field) */ protected function getDropColumnSql($table, $name) { - $sql = 'ALTER TABLE ' . $this->db->quoteName($table) . ' DROP COLUMN ' . $this->db->quoteName($name); - - return $sql; + return 'ALTER TABLE ' . $this->db->quoteName($table) . ' DROP COLUMN ' . $this->db->quoteName($name); } /** @@ -305,9 +305,7 @@ protected function getDropColumnSql($table, $name) */ protected function getDropKeySql($table, $name) { - $sql = 'ALTER TABLE ' . $this->db->quoteName($table) . ' DROP KEY ' . $this->db->quoteName($name); - - return $sql; + return 'ALTER TABLE ' . $this->db->quoteName($table) . ' DROP KEY ' . $this->db->quoteName($name); } /** @@ -321,9 +319,7 @@ protected function getDropKeySql($table, $name) */ protected function getDropPrimaryKeySql($table) { - $sql = 'ALTER TABLE ' . $this->db->quoteName($table) . ' DROP PRIMARY KEY'; - - return $sql; + return 'ALTER TABLE ' . $this->db->quoteName($table) . ' DROP PRIMARY KEY'; } /** @@ -374,11 +370,8 @@ 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 = ''; if ($kName == 'PRIMARY') @@ -390,24 +383,21 @@ protected function getKeySql($columns) $prefix = 'UNIQUE '; } - $nColumns = count($columns); $kColumns = array(); - if ($nColumns == 1) + foreach ($columns as $column) { - $kColumns[] = $this->db->quoteName($kColumn); - } - else - { - foreach ($columns as $column) + $kLength = ''; + + if (!empty($column['Sub_part'])) { - $kColumns[] = (string) $column['Column_name']; + $kLength = '(' . $column['Sub_part'] . ')'; } - } - $sql = $prefix . 'KEY ' . ($kName != 'PRIMARY' ? $this->db->quoteName($kName) : '') . ' (' . implode(',', $kColumns) . ')'; + $kColumns[] = $this->db->quoteName((string) $column['Column_name']) . $kLength; + } - return $sql; + return $prefix . 'KEY ' . ($kName != 'PRIMARY' ? $this->db->quoteName($kName) : '') . ' (' . implode(',', $kColumns) . ')'; } /** @@ -434,4 +424,47 @@ public function check() return $this; } + + /** + * Get the SQL syntax to add a table. + * + * @param SimpleXMLElement $table The table information. + * + * @return string + * + * @since __DEPLOY_VERSION__ + * @throws RuntimeException + */ + protected function xmlToCreate(SimpleXMLElement $table) + { + $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) . ', '; + } + + $newLookup = $this->getKeyLookup($table->xpath('key')); + + // Loop through each key in the new structure. + foreach ($newLookup as $key) + { + $createTableStatement .= $this->getKeySQL($key) . ', '; + } + + // Remove the comma after the last key + $createTableStatement = rtrim($createTableStatement, ', '); + + $createTableStatement .= ')'; + + return $createTableStatement; + } } diff --git a/libraries/joomla/database/importer/postgresql.php b/libraries/joomla/database/importer/postgresql.php index f64e6649ae50a..35261ab54261d 100644 --- a/libraries/joomla/database/importer/postgresql.php +++ b/libraries/joomla/database/importer/postgresql.php @@ -42,6 +42,94 @@ public function check() return $this; } + /** + * Get the SQL syntax to add a table. + * + * @param SimpleXMLElement $table The table information. + * + * @return string + * + * @since __DEPLOY_VERSION__ + * @throws RuntimeException + */ + protected function xmlToCreate(SimpleXMLElement $table) + { + $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; + } + + /** + * Get the details list of keys for a table. + * + * @param array $keys An array of objects that comprise the keys for the table. + * + * @return array The lookup array. array({key name} => array(object, ...)) + * + * @since __DEPLOY_VERSION__ + * @throws Exception + */ + protected function getKeyLookup($keys) + { + // First pass, create a lookup of the keys. + $lookup = array(); + + foreach ($keys as $key) + { + if ($key instanceof SimpleXMLElement) + { + $kName = (string) $key['Key_name']; + } + else + { + $kName = $key->Key_name; + } + + if (empty($lookup[$kName])) + { + $lookup[$kName] = array(); + } + + $lookup[$kName][] = $key; + } + + return $lookup; + } + /** * Get the SQL syntax to add a column. * @@ -124,6 +212,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. @@ -133,6 +222,7 @@ protected function getAlterTableSql(SimpleXMLElement $structure) { // The sequence is new $alters[] = $this->getAddSequenceSql($newSequenceLook[$kSeqName][0]); + $alters[] = $this->getSetvalSequenceSql($newSequenceLook[$kSeqName][0]); } } @@ -283,7 +373,7 @@ protected function getAddSequenceSql($field) $field['Start_Value'] = '1'; } - return 'CREATE SEQUENCE ' . (string) $field['Name'] . + return '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' . @@ -317,6 +407,22 @@ protected function getChangeSequenceSql($field) ' OWNED BY ' . $this->db->quoteName((string) $field['Schema'] . '.' . (string) $field['Table'] . '.' . (string) $field['Column']); } + /** + * 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. * @@ -399,15 +505,20 @@ protected function getAlterColumnSql($table, $field) */ protected function getColumnSql(SimpleXMLElement $field) { - // TODO Incorporate into parent class and use $this. - $blobs = array('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; + } /* nextval() as default value means that type field is serial */ if (strpos($fDefault, 'nextval') !== false) @@ -420,7 +531,7 @@ protected function getColumnSql(SimpleXMLElement $field) if ($fNull == 'NO') { - if (in_array($fType, $blobs) || $fDefault === null) + if ($fDefault === null) { $query .= ' NOT NULL'; } @@ -507,6 +618,34 @@ protected function getIdxLookup($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. * diff --git a/libraries/joomla/database/importer/sqlsrv.php b/libraries/joomla/database/importer/sqlsrv.php new file mode 100644 index 0000000000000..79cd12aedb01a --- /dev/null +++ b/libraries/joomla/database/importer/sqlsrv.php @@ -0,0 +1,413 @@ +db instanceof JDatabaseDriverSqlsrv)) + { + throw new Exception('JPLATFORM_ERROR_DATABASE_CONNECTOR_WRONG_TYPE'); + } + + // Check if the tables have been specified. + if (empty($this->from)) + { + throw new Exception('JPLATFORM_ERROR_NO_TABLES_SPECIFIED'); + } + + return $this; + } + + /** + * Get the SQL syntax to add a table. + * + * @param SimpleXMLElement $table The table information. + * + * @return string + * + * @since __DEPLOY_VERSION__ + * @throws RuntimeException + */ + protected function xmlToCreate(SimpleXMLElement $table) + { + $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) . ', '; + } + + // Remove the comma after the last key + $createTableStatement = rtrim($createTableStatement, ', '); + + $createTableStatement .= ')'; + + return $createTableStatement; + } + + /** + * Get the SQL syntax to add a column. + * + * @param string $table The table name. + * @param SimpleXMLElement $field The XML field definition. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + protected function getAddColumnSql($table, SimpleXMLElement $field) + { + return 'ALTER TABLE ' . $this->db->quoteName($table) . ' ADD COLUMN ' . $this->getColumnSql($field); + } + + /** + * Get the SQL syntax to add a key. + * + * @param string $table The table name. + * @param array $keys An array of the fields pertaining to this key. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + protected function getAddKeySql($table, $keys) + { + return 'ALTER TABLE ' . $this->db->quoteName($table) . ' ADD ' . $this->getKeySql($keys); + } + + /** + * Get alters for table if there is a difference. + * + * @param SimpleXMLElement $structure The XML structure pf the table. + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + protected function getAlterTableSql(SimpleXMLElement $structure) + { + $table = $this->getRealTableName($structure['name']); + $oldFields = $this->db->getTableColumns($table); + $oldKeys = $this->db->getTableKeys($table); + $alters = array(); + + // Get the fields and keys from the XML that we are aiming for. + $newFields = $structure->xpath('field'); + $newKeys = $structure->xpath('key'); + + // Loop through each field in the new structure. + foreach ($newFields as $field) + { + $fName = (string) $field['Field']; + + if (isset($oldFields[$fName])) + { + // The field exists, check it's the same. + $column = $oldFields[$fName]; + + // Test whether there is a change. + $change = ((string) $field['Type'] != $column->Type) || ((string) $field['Null'] != $column->Null) + || ((string) $field['Default'] != $column->Default) || ((string) $field['Extra'] != $column->Extra); + + if ($change) + { + $alters[] = $this->getChangeColumnSql($table, $field); + } + + // Unset this field so that what we have left are fields that need to be removed. + unset($oldFields[$fName]); + } + else + { + // The field is new. + $alters[] = $this->getAddColumnSql($table, $field); + } + } + + // Any columns left are orphans + foreach ($oldFields as $name => $column) + { + // Delete the column. + $alters[] = $this->getDropColumnSql($table, $name); + } + + // Get the lookups for the old and new keys. + $oldLookup = $this->getKeyLookup($oldKeys); + $newLookup = $this->getKeyLookup($newKeys); + + // Loop through each key in the new structure. + foreach ($newLookup as $name => $keys) + { + // Check if there are keys on this field in the existing table. + if (isset($oldLookup[$name])) + { + $same = true; + $newCount = count($newLookup[$name]); + $oldCount = count($oldLookup[$name]); + + // There is a key on this field in the old and new tables. Are they the same? + if ($newCount == $oldCount) + { + // Need to loop through each key and do a fine grained check. + for ($i = 0; $i < $newCount; $i++) + { + $same = (((string) $newLookup[$name][$i]['Non_unique'] == $oldLookup[$name][$i]->Non_unique) + && ((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]['Index_type'] == $oldLookup[$name][$i]->Index_type)); + + if (!$same) + { + // Break out of the loop. No need to check further. + break; + } + } + } + else + { + // Count is different, just drop and add. + $same = false; + } + + if (!$same) + { + $alters[] = $this->getDropKeySql($table, $name); + $alters[] = $this->getAddKeySql($table, $keys); + } + + // Unset this field so that what we have left are fields that need to be removed. + unset($oldLookup[$name]); + } + else + { + // This is a new key. + $alters[] = $this->getAddKeySql($table, $keys); + } + } + + // Any keys left are orphans. + foreach ($oldLookup as $name => $keys) + { + if (strtoupper($name) == 'PRIMARY') + { + $alters[] = $this->getDropPrimaryKeySql($table); + } + else + { + $alters[] = $this->getDropKeySql($table, $name); + } + } + + return $alters; + } + + /** + * Get the syntax to alter a column. + * + * @param string $table The name of the database table to alter. + * @param SimpleXMLElement $field The XML definition for the field. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + protected function getChangeColumnSql($table, SimpleXMLElement $field) + { + return 'ALTER TABLE ' . $this->db->quoteName($table) . ' CHANGE COLUMN ' . $this->db->quoteName((string) $field['Field']) . ' ' + . $this->getColumnSql($field); + } + + /** + * Get the SQL syntax for a single column that would be included in a table create or alter statement. + * + * @param SimpleXMLElement $field The XML field definition. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + protected function getColumnSql(SimpleXMLElement $field) + { + // TODO Incorporate into parent class and use $this. + $blobs = array('text', 'smalltext', 'mediumtext', 'largetext'); + + $fName = (string) $field['Field']; + $fType = (string) $field['Type']; + $fNull = (string) $field['Null']; + $fDefault = isset($field['Default']) ? (string) $field['Default'] : null; + $fExtra = (string) $field['Extra']; + + $query = $this->db->quoteName($fName) . ' ' . $fType; + + if ($fNull == 'NO') + { + if (in_array($fType, $blobs) || $fDefault === null) + { + $query .= ' NOT NULL'; + } + else + { + // TODO Don't quote numeric values. + $query .= ' NOT NULL DEFAULT ' . $this->db->quote($fDefault); + } + } + else + { + if ($fDefault === null) + { + $query .= ' DEFAULT NULL'; + } + else + { + // TODO Don't quote numeric values. + $query .= ' DEFAULT ' . $this->db->quote($fDefault); + } + } + + if ($fExtra) + { + $query .= ' ' . strtoupper($fExtra); + } + + return $query; + } + + /** + * Get the SQL syntax to drop a key. + * + * @param string $table The table name. + * @param string $name The name of the key to drop. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + protected function getDropKeySql($table, $name) + { + return 'ALTER TABLE ' . $this->db->quoteName($table) . ' DROP KEY ' . $this->db->quoteName($name); + } + + /** + * Get the SQL syntax to drop a key. + * + * @param string $table The table name. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + protected function getDropPrimaryKeySql($table) + { + return 'ALTER TABLE ' . $this->db->quoteName($table) . ' DROP PRIMARY KEY'; + } + + /** + * Get the details list of keys for a table. + * + * @param array $keys An array of objects that comprise the keys for the table. + * + * @return array The lookup array. array({key name} => array(object, ...)) + * + * @since __DEPLOY_VERSION__ + * @throws Exception + */ + protected function getKeyLookup($keys) + { + // First pass, create a lookup of the keys. + $lookup = array(); + + foreach ($keys as $key) + { + if ($key instanceof SimpleXMLElement) + { + $kName = (string) $key['Key_name']; + } + else + { + $kName = $key->Key_name; + } + + if (empty($lookup[$kName])) + { + $lookup[$kName] = array(); + } + + $lookup[$kName][] = $key; + } + + return $lookup; + } + + /** + * Get the SQL syntax for a key. + * + * @param array $columns An array of SimpleXMLElement objects comprising the key. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + protected function getKeySql($columns) + { + $kNonUnique = (string) $columns[0]['Non_unique']; + $kName = (string) $columns[0]['Key_name']; + $prefix = ''; + + if ($kName == 'PRIMARY') + { + $prefix = 'PRIMARY '; + } + elseif ($kNonUnique == 0) + { + $prefix = 'UNIQUE '; + } + + $kColumns = array(); + + foreach ($columns as $column) + { + $kLength = ''; + + if (!empty($column['Sub_part'])) + { + $kLength = '(' . $column['Sub_part'] . ')'; + } + + $kColumns[] = $this->db->quoteName((string) $column['Column_name']) . $kLength; + } + + return $prefix . 'KEY ' . ($kName != 'PRIMARY' ? $this->db->quoteName($kName) : '') . ' (' . implode(',', $kColumns) . ')'; + } +} diff --git a/tests/unit/suites/database/driver/mysql/JDatabaseExporterMysqlTest.php b/tests/unit/suites/database/driver/mysql/JDatabaseExporterMysqlTest.php index 6116db3da2d20..cc192b8a15a4b 100644 --- a/tests/unit/suites/database/driver/mysql/JDatabaseExporterMysqlTest.php +++ b/tests/unit/suites/database/driver/mysql/JDatabaseExporterMysqlTest.php @@ -21,7 +21,7 @@ class JDatabaseExporterMysqlTest extends TestCase - + '; @@ -223,7 +223,7 @@ public function testBuildXmlStructure() ' ', ' ', ' ', + 'Null="" Index_type="BTREE" Sub_part="" Comment="" />', ' ' ), TestReflection::invoke($instance, 'buildXmlStructure') diff --git a/tests/unit/suites/database/driver/mysqli/JDatabaseExporterMysqliTest.php b/tests/unit/suites/database/driver/mysqli/JDatabaseExporterMysqliTest.php index 7679fa06cdf29..b80bb8eb02d93 100644 --- a/tests/unit/suites/database/driver/mysqli/JDatabaseExporterMysqliTest.php +++ b/tests/unit/suites/database/driver/mysqli/JDatabaseExporterMysqliTest.php @@ -129,7 +129,7 @@ public function test__toString() - + '; @@ -180,7 +180,7 @@ public function testBuildXml() - + '; @@ -210,7 +210,7 @@ public function testBuildXmlStructure() ' ', ' ', ' ', + 'Null="" Index_type="BTREE" Sub_part="" Comment="" />', ' ' ), TestReflection::invoke($instance, 'buildXmlStructure') diff --git a/tests/unit/suites/database/driver/pdomysql/JDatabaseExporterPdomysqlTest.php b/tests/unit/suites/database/driver/pdomysql/JDatabaseExporterPdomysqlTest.php index c9d4deb775ca0..197dbbdaab6d7 100644 --- a/tests/unit/suites/database/driver/pdomysql/JDatabaseExporterPdomysqlTest.php +++ b/tests/unit/suites/database/driver/pdomysql/JDatabaseExporterPdomysqlTest.php @@ -112,6 +112,7 @@ public function setup() 'Packed' => '', 'Null' => '', 'Index_type' => 'BTREE', + 'Sub_part' => '', 'Comment' => '', ) ) @@ -227,7 +228,7 @@ public function test__toString() - + '; @@ -290,7 +291,7 @@ public function testBuildXml() - + '; @@ -330,7 +331,7 @@ public function testBuildXmlStructure() ' ', ' ', ' ', + 'Null="" Index_type="BTREE" Sub_part="" Comment="" />', ' ' ) ), diff --git a/tests/unit/suites/database/driver/postgresql/JDatabaseDriverPostgresqlTest.php b/tests/unit/suites/database/driver/postgresql/JDatabaseDriverPostgresqlTest.php index 3fffcfb33622c..a887fe1102fa8 100644 --- a/tests/unit/suites/database/driver/postgresql/JDatabaseDriverPostgresqlTest.php +++ b/tests/unit/suites/database/driver/postgresql/JDatabaseDriverPostgresqlTest.php @@ -395,24 +395,28 @@ public function testGetTableKeys() $pkey->idxName = 'jos_assets_pkey'; $pkey->isPrimary = 't'; $pkey->isUnique = 't'; + $pkey->indKey = '1'; $pkey->Query = 'ALTER TABLE jos_assets ADD PRIMARY KEY (id)'; $asset = new stdClass; $asset->idxName = 'idx_asset_name'; $asset->isPrimary = 'f'; $asset->isUnique = 't'; + $asset->indKey = '6'; $asset->Query = 'CREATE UNIQUE INDEX idx_asset_name ON jos_assets USING btree (name)'; $lftrgt = new stdClass; $lftrgt->idxName = 'jos_assets_idx_lft_rgt'; $lftrgt->isPrimary = 'f'; $lftrgt->isUnique = 'f'; + $lftrgt->indKey = '3 4'; $lftrgt->Query = 'CREATE INDEX jos_assets_idx_lft_rgt ON jos_assets USING btree (lft, rgt)'; $id = new stdClass; $id->idxName = 'jos_assets_idx_parent_id'; $id->isPrimary = 'f'; $id->isUnique = 'f'; + $id->indKey = '2'; $id->Query = 'CREATE INDEX jos_assets_idx_parent_id ON jos_assets USING btree (parent_id)'; $this->assertEquals(array($pkey, $id, $lftrgt, $asset), self::$driver->getTableKeys('jos_assets')); diff --git a/tests/unit/suites/database/driver/postgresql/JDatabaseExporterPostgresqlTest.php b/tests/unit/suites/database/driver/postgresql/JDatabaseExporterPostgresqlTest.php index 6da5825064ee3..000bd7a3200b6 100644 --- a/tests/unit/suites/database/driver/postgresql/JDatabaseExporterPostgresqlTest.php +++ b/tests/unit/suites/database/driver/postgresql/JDatabaseExporterPostgresqlTest.php @@ -38,7 +38,7 @@ class JDatabaseExporterPostgresqlTest extends TestCase protected function setup() { // Set up the database object mock. - $this->dbo = $this->getMockDatabase('Postgresql', array('getTableSequences'), '1970-01-01 00:00:00', 'Y-m-d H:i:s'); + $this->dbo = $this->getMockDatabase('Postgresql', array('getTableSequences', 'getSequenceLastValue', 'getSequenceIsCalled', 'getNamesKey'), '1970-01-01 00:00:00', 'Y-m-d H:i:s'); $this->dbo->expects($this->any()) ->method('getPrefix') @@ -52,28 +52,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' => '', ) ) @@ -86,11 +86,16 @@ 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') + ->willReturn('id'); + // Check if database is at least 9.1.0 $this->dbo->expects($this->any()) ->method('getVersion') @@ -132,6 +137,14 @@ protected function setup() ->method('loadObjectList') ->willReturn(array()); + $this->dbo->expects($this->any()) + ->method('getSequenceLastValue') + ->willReturn('1'); + + $this->dbo->expects($this->any()) + ->method('getSequenceIsCalled') + ->willReturn('f'); + $this->dbo->expects($this->any()) ->method('getTableList') ->willReturn(array('jos_dbtest')); @@ -192,7 +205,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 @@ -207,14 +220,14 @@ public function test__toString() $expecting = ' - - + + - + '; @@ -258,7 +271,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 @@ -273,14 +286,14 @@ public function testBuildXml() $expecting = ' - - + + - + '; @@ -302,7 +315,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 @@ -316,14 +329,14 @@ public function testBuildXmlStructure() $this->assertEquals( array( - ' ', - ' ', + ' ', + ' ', ' ', ' ', ' ', ' ', - ' ', + ' ', ' ' ), TestReflection::invoke($instance, 'buildXmlStructure') @@ -416,8 +429,8 @@ public function testGetGenericTableName() $instance->setDbo($this->dbo); $this->assertSame( - '#__test', - TestReflection::invoke($instance, 'getGenericTableName', 'jos_test'), + '#__dbtest', + TestReflection::invoke($instance, 'getGenericTableName', 'jos_dbtest'), 'The testGetGenericTableName should replace the database prefix with #__.' ); } diff --git a/tests/unit/suites/database/driver/postgresql/JDatabaseImporterPostgresqlTest.php b/tests/unit/suites/database/driver/postgresql/JDatabaseImporterPostgresqlTest.php index a56ae150611e2..e4b4e30f72e3c 100644 --- a/tests/unit/suites/database/driver/postgresql/JDatabaseImporterPostgresqlTest.php +++ b/tests/unit/suites/database/driver/postgresql/JDatabaseImporterPostgresqlTest.php @@ -41,6 +41,7 @@ public function setup() 'getAddSequenceSQL', 'getChangeSequenceSQL', 'getDropSequenceSQL', + 'getSetvalSequenceSql', 'getAddIndexSQL', 'getVersion', 'quoteName', @@ -100,12 +101,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)', ) ) @@ -269,26 +272,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( @@ -324,7 +328,7 @@ public function dataGetAlterTableSql() // Add sequence new SimpleXmlElement('' . $s1 . $s2 . $f1 . $f2 . $k1 . $k2 . ''), array( - $addSequence, + $addSequence, $setValSequence, ), 'getAlterTableSQL should add the new sequence.' ), @@ -378,7 +382,7 @@ public function dataGetAlterTableSql() // Change seq new SimpleXmlElement('' . $s2 . $f1 . $f2 . $k1 . $k2 . ''), array( - $changeSeq, + $changeSeq, $setValSequence, "DROP SEQUENCE \"jos_dbtest_id_seq\"",), 'getAlterTableSQL should change sequence.' ), @@ -412,9 +416,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( @@ -546,7 +550,7 @@ public function testGetAddColumnSql() $instance->setDbo($this->dbo); $sample = array( - 'xml-title-field' => '', + 'xml-title-field' => '', 'xml-title-def' => '', 'xml-int-defnum' => '',); @@ -591,7 +595,7 @@ public function testGetAddSequenceSql() $this->assertThat( TestReflection::invoke($instance, 'getAddSequenceSQL', 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.' ); @@ -602,9 +606,9 @@ public function testGetAddSequenceSql() */ public function testGetAddIndexSql() { - $xmlIndex = ''; - $xmlPrimaryKey = ''; $instance = new JDatabaseImporterPostgresql;