summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/MySQLTableBuilder.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/MySQLTableBuilder.php')
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/MySQLTableBuilder.php402
1 files changed, 402 insertions, 0 deletions
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/MySQLTableBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/MySQLTableBuilder.php
new file mode 100644
index 00000000..7c9a5ebc
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/MySQLTableBuilder.php
@@ -0,0 +1,402 @@
+<?php
+
+namespace SMW\SQLStore\TableBuilder;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ * @author Marcel Gsteiger
+ * @author Jeroen De Dauw
+ */
+class MySQLTableBuilder extends TableBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getStandardFieldType( $fieldType ) {
+
+ $charLongLength = FieldType::CHAR_LONG_LENGTH;
+
+ $fieldTypes = [
+ // like page_id in MW page table
+ 'id' => 'INT(11) UNSIGNED',
+ // like page_id in MW page table
+ 'id_primary' => 'INT(11) UNSIGNED NOT NULL KEY AUTO_INCREMENT',
+
+ // (see postgres on the difference)
+ 'id_unsigned' => 'INT(11) UNSIGNED',
+
+ // like page_namespace in MW page table
+ 'namespace' => 'INT(11)',
+ // like page_title in MW page table
+ 'title' => 'VARBINARY(255)',
+ // like iw_prefix in MW interwiki table
+ 'interwiki' => 'VARBINARY(32)',
+ 'iw' => 'VARBINARY(32)',
+ 'hash' => 'VARBINARY(40)',
+ // larger blobs of character data, usually not subject to SELECT conditions
+ 'blob' => 'MEDIUMBLOB',
+ 'text' => 'TEXT',
+ 'boolean' => 'TINYINT(1)',
+ 'double' => 'DOUBLE',
+ 'integer' => 'INT(8)',
+ 'char_long' => "VARBINARY($charLongLength)",
+ 'char_nocase' => 'VARCHAR(255) CHARSET utf8 COLLATE utf8_general_ci',
+ 'char_long_nocase' => "VARCHAR($charLongLength) CHARSET utf8 COLLATE utf8_general_ci",
+ 'usage_count' => 'INT(8) UNSIGNED',
+ 'integer_unsigned' => 'INT(8) UNSIGNED'
+ ];
+
+ return FieldType::mapType( $fieldType, $fieldTypes );
+ }
+
+ /** Create */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doCreateTable( $tableName, array $attributes = null ) {
+
+ $tableName = $this->connection->tableName( $tableName );
+ $sql = '';
+
+ $fieldSql = [];
+ $fields = $attributes['fields'];
+
+ foreach ( $fields as $fieldName => $fieldType ) {
+ $fieldSql[] = "$fieldName " . $this->getStandardFieldType( $fieldType );
+ }
+
+ // @see $wgDBname
+ $dbName = isset( $this->config['wgDBname'] ) ? "`". $this->config['wgDBname'] . "`." : '';
+
+ $sql .= 'CREATE TABLE ' . $dbName . $tableName . ' (' . implode( ',', $fieldSql ) . ') ';
+ $sql .= $this->sql_from( $attributes );
+
+ $this->connection->query( $sql, __METHOD__ );
+ }
+
+ private function sql_from( array $attributes ) {
+
+ // $smwgFulltextSearchTableOptions can define:
+ // - 'mysql' => array( 'ENGINE=MyISAM, DEFAULT CHARSET=utf8' )
+ // - 'mysql' => array( 'ENGINE=MyISAM, DEFAULT CHARSET=utf8', 'WITH PARSER ngram' )
+ if ( isset( $attributes['fulltextSearchTableOptions']['mysql'] ) ) {
+
+ $tableOption = $attributes['fulltextSearchTableOptions']['mysql'];
+
+ // By convention the first index has table specific relevance
+ if ( is_array( $tableOption ) ) {
+ $tableOption = isset( $tableOption[0] ) ? $tableOption[0] : '';
+ }
+
+ return $tableOption;
+ }
+
+ // @see $wgDBTableOptions, This replacement is needed for compatibility,
+ // http://bugs.mysql.com/bug.php?id=17501
+ if ( isset( $this->config['wgDBTableOptions'] ) ) {
+ return str_replace( 'TYPE', 'ENGINE', $this->config['wgDBTableOptions'] );
+ }
+ }
+
+ /** Update */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doUpdateTable( $tableName, array $attributes = null ) {
+
+ $tableName = $this->connection->tableName( $tableName );
+ $currentFields = $this->getCurrentFields( $tableName );
+
+ $fields = $attributes['fields'];
+ $position = 'FIRST';
+
+ // Loop through all the field definitions, and handle each definition
+ foreach ( $fields as $fieldName => $fieldType ) {
+ $this->doUpdateField( $tableName, $fieldName, $fieldType, $currentFields, $position, $attributes );
+
+ $position = "AFTER $fieldName";
+ $currentFields[$fieldName] = false;
+ }
+
+ // The updated fields have their value set to false, so if a field has a value
+ // that differs from false, it's an obsolete one that should be removed.
+ foreach ( $currentFields as $fieldName => $value ) {
+ if ( $value !== false ) {
+ $this->doDropField( $tableName, $fieldName );
+ }
+ }
+ }
+
+ private function getCurrentFields( $tableName ) {
+
+ $sql = 'DESCRIBE ' . $tableName;
+
+ $res = $this->connection->query( $sql, __METHOD__ );
+ $currentFields = [];
+
+ foreach ( $res as $row ) {
+ $type = strtoupper( $row->Type );
+
+ if ( substr( $type, 0, 8 ) == 'VARCHAR(' ) {
+ $type .= ' binary'; // just assume this to be the case for VARCHAR, though DESCRIBE will not tell us
+ }
+
+ if ( $row->Null != 'YES' ) {
+ $type .= ' NOT NULL';
+ }
+
+ if ( $row->Key == 'PRI' ) { /// FIXME: updating "KEY" is not possible, the below query will fail in this case.
+ $type .= ' KEY';
+ }
+
+ if ( $row->Extra == 'auto_increment' ) {
+ $type .= ' AUTO_INCREMENT';
+ }
+
+ $currentFields[$row->Field] = $type;
+ }
+
+ return $currentFields;
+ }
+
+ private function doUpdateField( $tableName, $fieldName, $fieldType, $currentFields, $position, array $attributes ) {
+
+ if ( !isset( $this->activityLog[$tableName] ) ) {
+ $this->activityLog[$tableName] = [];
+ }
+
+ $fieldType = $this->getStandardFieldType( $fieldType );
+ $default = '';
+
+ if ( isset( $attributes['defaults'][$fieldName] ) ) {
+ $default = "DEFAULT '" . $attributes['defaults'][$fieldName] . "'";
+ }
+
+ if ( !array_key_exists( $fieldName, $currentFields ) ) {
+ $this->doCreateField( $tableName, $fieldName, $position, $fieldType, $default );
+ } elseif ( $currentFields[$fieldName] != $fieldType ) {
+ $this->doUpdateFieldType( $tableName, $fieldName, $position, $currentFields[$fieldName], $fieldType );
+ } else {
+ $this->reportMessage( " ... field $fieldName is fine.\n" );
+ }
+ }
+
+ private function doCreateField( $tableName, $fieldName, $position, $fieldType, $default ) {
+
+ $this->activityLog[$tableName][$fieldName] = self::PROC_FIELD_NEW;
+
+ $this->reportMessage( " ... creating field $fieldName ... " );
+ $this->connection->query( "ALTER TABLE $tableName ADD `$fieldName` $fieldType $default $position", __METHOD__ );
+ $this->reportMessage( "done.\n" );
+ }
+
+ private function doUpdateFieldType( $tableName, $fieldName, $position, $oldFieldType, $newFieldType ) {
+
+ $this->activityLog[$tableName][$fieldName] = self::PROC_FIELD_UPD;
+
+ // Continue to alter the type but silence the output since we cannot get
+ // any better information from MySQL about the types hence we a hack the
+ // message
+ if ( strpos( $oldFieldType, 'binary' ) !== false && strpos( $newFieldType, 'CHARSET utf8 COLLATE utf8_general_ci' ) !== false ) {
+ $this->reportMessage( " ... changing to a CHARSET utf8 field type ... " );
+ } else {
+ $this->reportMessage( " ... changing type of field $fieldName from '$oldFieldType' to '$newFieldType' ... " );
+ }
+
+ // To avoid Error: 1068 Multiple primary key defined when a PRIMARY is involved
+ if ( strpos( $newFieldType, 'AUTO_INCREMENT' ) !== false ) {
+ $this->connection->query( "ALTER TABLE $tableName DROP PRIMARY KEY", __METHOD__ );
+ }
+
+ $this->connection->query( "ALTER TABLE $tableName CHANGE `$fieldName` `$fieldName` $newFieldType $position", __METHOD__ );
+
+ // http://stackoverflow.com/questions/1873085/how-to-convert-from-varbinary-to-char-varchar-in-mysql
+ // http://bugs.mysql.com/bug.php?id=34564
+ if ( strpos( $oldFieldType, 'VARBINARY' ) !== false && strpos( $newFieldType, 'VARCHAR' ) !== false ) {
+ // $this->connection->query( "SELECT CAST($fieldName AS CHAR) from $tableName", __METHOD__ );
+ }
+
+ $this->reportMessage( "done.\n" );
+ }
+
+ private function doDropField( $tableName, $fieldName ) {
+
+ $this->activityLog[$tableName][$fieldName] = self::PROC_FIELD_DROP;
+
+ $this->reportMessage( " ... deleting obsolete field $fieldName ... " );
+ $this->connection->query( "ALTER TABLE $tableName DROP COLUMN `$fieldName`", __METHOD__ );
+ $this->reportMessage( "done.\n" );
+ }
+
+ /** Index */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doCreateIndices( $tableName, array $indexOptions = null ) {
+
+ $indices = $indexOptions['indices'];
+
+ // First remove possible obsolete indices
+ $this->doDropObsoleteIndices( $tableName, $indices );
+
+ // Add new indexes.
+ foreach ( $indices as $indexName => $index ) {
+ // If the index is an array, it contains the column
+ // name as first element, and index type as second one.
+ if ( is_array( $index ) ) {
+ $columns = $index[0];
+ $indexType = count( $index ) > 1 ? $index[1] : 'INDEX';
+ } else {
+ $columns = $index;
+ $indexType = 'INDEX';
+ }
+
+ $this->doCreateIndex( $tableName, $indexType, $indexName, $columns, $indexOptions );
+ }
+ }
+
+ private function doDropObsoleteIndices( $tableName, array &$indices ) {
+
+ $tableName = $this->connection->tableName( $tableName );
+ $currentIndices = $this->getIndexInfo( $tableName );
+
+ $idx = [];
+
+ // #2717
+ // The index info doesn't return length information (...idx1(200),idx2...)
+ // for an index hence to avoid a constant remove/create cycle we eliminate
+ // the length information from the temporary mirror when comparing new and
+ // old; of course we won't detect length changes!
+ foreach ( $indices as $k => $columns ) {
+ $idx[$k] = preg_replace("/\([^)]+\)/", "", $columns );
+ }
+
+ foreach ( $currentIndices as $indexName => $indexColumn ) {
+ // Indices may contain something like array( 'id', 'UNIQUE INDEX' )
+ $id = $this->recursive_array_search( $indexColumn, $idx );
+ if ( $id !== false || $indexName == 'PRIMARY' ) {
+ $this->reportMessage( " ... index $indexColumn is fine.\n" );
+
+ if ( $id !== false ) {
+ unset( $indices[$id] );
+ unset( $idx[$id] );
+ }
+
+ } else { // Duplicate or unrequired index.
+ $this->doDropIndex( $tableName, $indexName, $indexColumn );
+ }
+ }
+ }
+
+ /**
+ * Get the information about all indexes of a table. The result is an
+ * array of format indexname => indexcolumns. The latter is a comma
+ * separated list.
+ *
+ * @return array indexname => columns
+ */
+ private function getIndexInfo( $tableName ) {
+
+ $indices = [];
+
+ $res = $this->connection->query( 'SHOW INDEX FROM ' . $tableName, __METHOD__ );
+
+ if ( !$res ) {
+ return $indices;
+ }
+
+ foreach ( $res as $row ) {
+ if ( !array_key_exists( $row->Key_name, $indices ) ) {
+ $indices[$row->Key_name] = $row->Column_name;
+ } else {
+ $indices[$row->Key_name] .= ',' . $row->Column_name;
+ }
+ }
+
+ return $indices;
+ }
+
+ private function doDropIndex( $tableName, $indexName, $columns ) {
+ $this->reportMessage( " ... removing index $columns ..." );
+ $this->connection->query( 'DROP INDEX ' . $indexName . ' ON ' . $tableName, __METHOD__ );
+ $this->reportMessage( "done.\n" );
+ }
+
+ private function doCreateIndex( $tableName, $indexType, $indexName, $columns, array $indexOptions ) {
+
+ $tableName = $this->connection->tableName( $tableName );
+ $indexOption = '';
+
+ $this->reportMessage( " ... creating new index $columns ..." );
+
+ // @see MySQLTableBuilder::createExtraSQLFromattributes
+ // @see https://dev.mysql.com/doc/refman/5.7/en/fulltext-search-ngram.html
+ if ( isset( $indexOptions['fulltextSearchTableOptions']['mysql'] ) ) {
+ $indexOption = $indexOptions['fulltextSearchTableOptions']['mysql'];
+
+ // By convention the second index has index specific relevance
+ if ( is_array( $indexOption ) ) {
+ $indexOption = isset( $indexOption[1] ) ? $indexOption[1] : '';
+ }
+ }
+
+ if ( $indexType === 'FULLTEXT' ) {
+ $this->connection->query( "ALTER TABLE $tableName ADD $indexType $columns ($columns) $indexOption", __METHOD__ );
+ } else {
+ $this->connection->query( "ALTER TABLE $tableName ADD $indexType ($columns)", __METHOD__ );
+ }
+
+ $this->reportMessage( "done.\n" );
+ }
+
+ /** Drop */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doDropTable( $tableName ) {
+ $this->connection->query( 'DROP TABLE ' . $this->connection->tableName( $tableName ), __METHOD__ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ protected function doOptimize( $tableName ) {
+
+ $this->reportMessage( "Checking table $tableName ...\n" );
+
+ // https://dev.mysql.com/doc/refman/5.7/en/analyze-table.html
+ // Performs a key distribution analysis and stores the distribution for
+ // the named table or tables
+ $this->reportMessage( " ... analyze" );
+ $this->connection->query( 'ANALYZE TABLE ' . $this->connection->tableName( $tableName ), __METHOD__ );
+
+ // https://dev.mysql.com/doc/refman/5.7/en/optimize-table.html
+ // Reorganizes the physical storage of table data and associated index data,
+ // to reduce storage space and improve I/O efficiency
+ $this->reportMessage( ", optimize " );
+ $this->connection->query( 'OPTIMIZE TABLE ' . $this->connection->tableName( $tableName ), __METHOD__ );
+
+ $this->reportMessage( "done.\n" );
+ }
+
+}