Completed
Push — master ( 5d1976...30add5 )
by mw
13s
created

SMWSQLHelpers   D

Complexity

Total Complexity 86

Size/Duplication

Total Lines 521
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 60.29%
Metric Value
wmc 86
lcom 1
cbo 0
dl 0
loc 521
ccs 126
cts 209
cp 0.6029
rs 4.8717

12 Methods

Rating   Name   Duplication   Size   Complexity  
B getStandardDBType() 0 18 13
A setupTable() 0 15 2
B createTable() 0 20 6
C updateTable() 0 42 8
C getFields() 0 78 14
D updatePostgresField() 0 41 9
B updateMySqlField() 0 23 4
D setupIndex() 0 46 10
C getIndexInfo() 0 58 11
A dropIndex() 0 13 3
A createIndex() 0 15 4
A reportProgress() 0 5 2

How to fix   Complexity   

Complex Class

Complex classes like SMWSQLHelpers often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SMWSQLHelpers, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Some static helper functions that SMW uses for setting up
5
 * SQL databases.
6
 *
7
 * @author Markus Krötzsch
8
 * @author Marcel Gsteiger
9
 * @author Jeroen De Dauw
10
 *
11
 * @ingroup SMWStore
12
 */
13
class SMWSQLHelpers {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
14
15
	/**
16
	 * Database backends often have different types that need to be used
17
	 * repeatedly in (Semantic) MediaWiki. This function provides the
18
	 * preferred type (as a string) for various common kinds of columns.
19
	 * The input is one of the following strings: 'id' (page id numbers or
20
	 * similar), 'title' (title strings or similar), 'namespace' (namespace
21
	 * numbers), 'blob' (longer text blobs), 'iw' (interwiki prefixes).
22
	 *
23
	 * @param string $input
24
	 * @return string|false SQL type declaration
25
	 */
26 3
	static public function getStandardDBType( $input ) {
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
27 3
		global $wgDBtype;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
28
29
		switch ( $input ) {
30 3
			case 'id':
31 3
			return $wgDBtype == 'postgres' ? 'SERIAL' : ($wgDBtype == 'sqlite' ? 'INTEGER' :'INT(8) UNSIGNED'); // like page_id in MW page table
32 3
			case 'namespace':
33 3
			return $wgDBtype == 'postgres' ? 'BIGINT' : 'INT(11)'; // like page_namespace in MW page table
34 3
			case 'title':
35 3
			return $wgDBtype == 'postgres' ? 'TEXT' : 'VARBINARY(255)'; // like page_title in MW page table
36 3
			case 'iw':
37 3
			return ($wgDBtype == 'postgres' || $wgDBtype == 'sqlite') ? 'TEXT' : 'VARBINARY(32)'; // like iw_prefix in MW interwiki table
38 3
			case 'blob':
39 3
			return $wgDBtype == 'postgres' ? 'BYTEA' : 'MEDIUMBLOB'; // larger blobs of character data, usually not subject to SELECT conditions
40
		}
41
42
		return false;
43
	}
44
45
	/**
46
	 * Generic creation and updating function for database tables. Ideally, it
47
	 * would be able to modify a table's signature in arbitrary ways, but it will
48
	 * fail for some changes. Its string-based interface is somewhat too
49
	 * impoverished for a permanent solution. It would be possible to go for update
50
	 * scripts (specific to each change) in the style of MediaWiki instead.
51
	 *
52
	 * Make sure the table of the given name has the given fields, provided
53
	 * as an array with entries fieldname => typeparams. typeparams should be
54
	 * in a normalised form and order to match to existing values.
55
	 *
56
	 * The function returns an array that includes all columns that have been
57
	 * changed. For each such column, the array contains an entry
58
	 * columnname => action, where action is one of 'up', 'new', or 'del'
59
	 *
60
	 * If progress reports during this operation are desired, then the parameter $reportTo should
61
	 * be given an object that has a method reportProgress(string) for doing so.
62
	 *
63
	 * @note The function partly ignores the order in which fields are set up.
64
	 * Only if the type of some field changes will its order be adjusted explicitly.
65
	 *
66
	 * @param string $tableName The table name. Does not need to have been passed to DatabaseBase->tableName yet.
0 ignored issues
show
Documentation introduced by
There is no parameter named $tableName. Did you maybe mean $rawTableName?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
67
	 * @param array $columns The fields and their types the table should have.
0 ignored issues
show
Bug introduced by
There is no parameter named $columns. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
68
	 * @param DatabaseBase or Database $db
69
	 * @param object $reportTo Object to report back to.
70
	 */
71 3
	public static function setupTable( $rawTableName, array $fields, $db, $reportTo = null ) {
72 3
		$tableName = $db->tableName( $rawTableName );
73
74 3
		self::reportProgress( "Checking table $tableName ...\n", $reportTo );
0 ignored issues
show
Bug introduced by
It seems like $reportTo defined by parameter $reportTo on line 71 can also be of type null; however, SMWSQLHelpers::reportProgress() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
75
76 3
		if ( $db->tableExists( $rawTableName ) === false ) { // create new table
77 2
			self::reportProgress( "   Table not found, now creating...\n", $reportTo );
0 ignored issues
show
Bug introduced by
It seems like $reportTo defined by parameter $reportTo on line 71 can also be of type null; however, SMWSQLHelpers::reportProgress() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
78 2
			self::createTable( $tableName, $fields, $db );
79 2
			self::reportProgress( "   ... done.\n", $reportTo );
0 ignored issues
show
Bug introduced by
It seems like $reportTo defined by parameter $reportTo on line 71 can also be of type null; however, SMWSQLHelpers::reportProgress() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
80
		} else {
81 2
			self::reportProgress( "   Table already exists, checking structure ...\n", $reportTo );
0 ignored issues
show
Bug introduced by
It seems like $reportTo defined by parameter $reportTo on line 71 can also be of type null; however, SMWSQLHelpers::reportProgress() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
82 2
			self::updateTable( $tableName, $fields, $db, $reportTo );
0 ignored issues
show
Bug introduced by
It seems like $reportTo defined by parameter $reportTo on line 71 can also be of type null; however, SMWSQLHelpers::updateTable() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
83 2
			self::reportProgress( "   ... done.\n", $reportTo );
0 ignored issues
show
Bug introduced by
It seems like $reportTo defined by parameter $reportTo on line 71 can also be of type null; however, SMWSQLHelpers::reportProgress() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
84
		}
85 3
	}
86
87
	/**
88
	 * Creates a new database table with the specified fields.
89
	 *
90
	 * @param string $tableName The table name.
91
	 * @param array $columns The fields and their types the table should have.
0 ignored issues
show
Bug introduced by
There is no parameter named $columns. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
92
	 * @param DatabaseBase|Database $db
93
	 */
94 2
	private static function createTable( $tableName, array $fields, $db ) {
0 ignored issues
show
Coding Style introduced by
createTable uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
95 2
		global $wgDBtype, $wgDBname;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
96
97 2
		$sql = 'CREATE TABLE ' . ( ( $wgDBtype == 'postgres' || $wgDBtype == 'sqlite' ) ? '' : "`$wgDBname`." ) . $tableName . ' (';
98
99 2
		$fieldSql = array();
100
101 2
		foreach ( $fields as $fieldName => $fieldType ) {
102 2
			$fieldSql[] = "$fieldName  $fieldType";
103
		}
104
105 2
		$sql .= implode( ',', $fieldSql ) . ') ';
106
107 2
		if ( $wgDBtype != 'postgres' && $wgDBtype != 'sqlite' ) {
108
			// This replacement is needed for compatibility, see http://bugs.mysql.com/bug.php?id=17501
109 2
			$sql .= str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] );
110
		}
111
112 2
		$db->query( $sql, __METHOD__ );
113 2
	}
114
115
	/**
116
	 * Update a table given an array of field names and field types.
117
	 *
118
	 * @param string $tableName The table name.
119
	 * @param array $columns The fields and their types the table should have.
0 ignored issues
show
Bug introduced by
There is no parameter named $columns. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
120
	 * @param DatabaseBase|Database $db
121
	 * @param object $reportTo Object to report back to.
122
	 */
123 2
	private static function updateTable( $tableName, array $fields, $db, $reportTo ) {
124 2
		global $wgDBtype;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
125
126 2
		$currentFields = self::getFields( $tableName, $db, $reportTo );
127
128 2
		$isPostgres = $wgDBtype == 'postgres';
129
130 2
		if ( !$isPostgres ) {
131 2
			$position = 'FIRST';
132
		}
133
134
		// Loop through all the field definitions, and handle each definition for either postgres or MySQL.
135 2
		foreach ( $fields as $fieldName => $fieldType ) {
136 2
			if ( $isPostgres ) {
137
				self::updatePostgresField( $tableName, $fieldName, $fieldType, $currentFields, $db, $reportTo );
138
			} else {
139 2
				self::updateMySqlField( $tableName, $fieldName, $fieldType, $currentFields, $db, $reportTo, $position );
0 ignored issues
show
Bug introduced by
The variable $position does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
140 2
				$position = "AFTER $fieldName";
141
			}
142
143 2
			$currentFields[$fieldName] = false;
144
		}
145
146
		// The updated fields have their value set to false, so if a field has a value
147
		// that differs from false, it's an obsolete one that should be removed.
148 2
		foreach ( $currentFields as $fieldName => $value ) {
149 2
			if ( $value !== false ) {
150
				self::reportProgress( "   ... deleting obsolete field $fieldName ... ", $reportTo );
151
152
				if ( $isPostgres ) {
153
					$db->query( 'ALTER TABLE "' . $tableName . '" DROP COLUMN "' . $fieldName . '"', __METHOD__ );
154
				} elseif ( $wgDBtype == 'sqlite' ) {
155
					// DROP COLUMN not supported in Sqlite3
156
					self::reportProgress( "   ... deleting obsolete field $fieldName not possible in SQLLite ... you could delete and reinitialize the tables to remove obsolete data, or just keep it ... ", $reportTo );
157
				} else {
158
					$db->query( "ALTER TABLE $tableName DROP COLUMN `$fieldName`", __METHOD__ );
159
				}
160
161 2
				self::reportProgress( "done.\n", $reportTo );
162
			}
163
		}
164 2
	}
165
166
	/**
167
	 * Returns an array of fields (as keys) and their types (as values).
168
	 *
169
	 * @param string $tableName The table name.
170
	 * @param DatabaseBase|Database $db
171
	 * @param object $reportTo to report back to.
172
	 *
173
	 * @return array
174
	 */
175 2
	private static function getFields( $tableName, $db, $reportTo ) {
0 ignored issues
show
Unused Code introduced by
The parameter $reportTo is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
176 2
		global $wgDBtype;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
177
178 2
		if ( $wgDBtype == 'postgres' ) {
179
			$tableName = str_replace( '"', '', $tableName );
180
			// Use the data dictionary in postgresql to get an output comparable to DESCRIBE.
181
			$sql = <<<EOT
182
SELECT
183
	a.attname as "Field",
184
	upper(pg_catalog.format_type(a.atttypid, a.atttypmod)) as "Type",
185
	(SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)
186
	FROM pg_catalog.pg_attrdef d
187
	WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) as "Extra",
188
		case when a.attnotnull THEN 'NO'::text else 'YES'::text END as "Null", a.attnum
189
	FROM pg_catalog.pg_attribute a
190
	WHERE a.attrelid = (
191
	    SELECT c.oid
192
	    FROM pg_catalog.pg_class c
193
	    LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
194
	    WHERE c.relname ~ '^($tableName)$'
195
	    AND pg_catalog.pg_table_is_visible(c.oid)
196
	    LIMIT 1
197
	 ) AND a.attnum > 0 AND NOT a.attisdropped
198
	 ORDER BY a.attnum
199
EOT;
200 2
		} elseif ( $wgDBtype == 'sqlite' ) { // SQLite
201
			$sql = 'PRAGMA table_info(' . $tableName . ')';
202
		} else { // MySQL
203 2
			$sql = 'DESCRIBE ' . $tableName;
204
		}
205
206 2
		$res = $db->query( $sql, __METHOD__ );
207 2
		$curfields = array();
208
209 2
		foreach ( $res as $row ) {
210 2
			if ( $wgDBtype == 'postgres' ) { // postgresql
211
				$type = strtoupper( $row->Type );
212
213
				if ( preg_match( '/^nextval\\(.+\\)/i', $row->Extra ) ) {
214
					$type = 'SERIAL NOT NULL';
215
				} elseif ( $row->Null != 'YES' ) {
216
						$type .= ' NOT NULL';
217
				}
218 2
			} elseif ( $wgDBtype == 'sqlite' ) { // SQLite
219
				$row->Field = $row->name;
220
				$row->Type = $row->type;
221
				$type = $row->type;
222
				if ( $row->notnull == '1' ) {
223
					$type .= ' NOT NULL';
224
				}
225
				if ( $row->pk == '1' ) {
226
					$type .= ' PRIMARY KEY AUTOINCREMENT';
227
				}
228
			} else { // mysql
229 2
				$type = strtoupper( $row->Type );
230
231 2
				if ( substr( $type, 0, 8 ) == 'VARCHAR(' ) {
232
					$type .= ' binary'; // just assume this to be the case for VARCHAR, though DESCRIBE will not tell us
233
				}
234
235 2
				if ( $row->Null != 'YES' ) {
236 2
					$type .= ' NOT NULL';
237
				}
238
239 2
				if ( $row->Key == 'PRI' ) { /// FIXME: updating "KEY" is not possible, the below query will fail in this case.
240 2
					$type .= ' KEY';
241
				}
242
243 2
				if ( $row->Extra == 'auto_increment' ) {
244 2
					$type .= ' AUTO_INCREMENT';
245
				}
246
			}
247
248 2
			$curfields[$row->Field] = $type;
249
		}
250
251 2
		return $curfields;
252
	}
253
254
	/**
255
	 * Update a single field given it's name and type and an array of
256
	 * current fields. Postgres version.
257
	 *
258
	 * @param string $tableName The table name.
259
	 * @param string $name The field name.
260
	 * @param string $type The field type and attributes.
261
	 * @param array $currentFields List of fields as they have been found in the database.
262
	 * @param DatabaseBase|Database $db
263
	 * @param object $reportTo Object to report back to.
264
	 */
265
	private static function updatePostgresField( $tableName, $name, $type, array $currentFields, $db, $reportTo ) {
266
		$keypos = strpos( $type, ' PRIMARY KEY' );
267
268
		if ( $keypos > 0 ) {
269
			$type = substr( $type, 0, $keypos );
270
		}
271
272
		$type = strtoupper( $type );
273
274
		if ( !array_key_exists( $name, $currentFields ) ) {
275
			self::reportProgress( "   ... creating field $name ... ", $reportTo );
276
277
			$db->query( "ALTER TABLE $tableName ADD \"" . $name . "\" $type", __METHOD__ );
278
279
			self::reportProgress( "done.\n", $reportTo );
280
		} elseif ( $currentFields[$name] != $type ) {
281
			self::reportProgress( "   ... changing type of field $name from '$currentFields[$name]' to '$type' ... ", $reportTo );
282
283
			$notnullposnew = strpos( $type, ' NOT NULL' );
284
			if ( $notnullposnew > 0 ) {
285
				$type = substr( $type, 0, $notnullposnew );
286
			}
287
288
			$notnullposold = strpos( $currentFields[$name], ' NOT NULL' );
289
			$typeold  = strtoupper( ( $notnullposold > 0 ) ? substr( $currentFields[$name], 0, $notnullposold ) : $currentFields[$name] );
290
291
			if ( $typeold != $type ) {
292
				$sql = "ALTER TABLE " . $tableName . " ALTER COLUMN \"" . $name . "\" TYPE " . $type;
293
				$db->query( $sql, __METHOD__ );
294
			}
295
296
			if ( $notnullposold != $notnullposnew ) {
297
				$sql = "ALTER TABLE " . $tableName . " ALTER COLUMN \"" . $name . "\" " . ( $notnullposnew > 0 ? 'SET' : 'DROP' ) . " NOT NULL";
298
				$db->query( $sql, __METHOD__ );
299
			}
300
301
			self::reportProgress( "done.\n", $reportTo );
302
		} else {
303
			self::reportProgress( "   ... field $name is fine.\n", $reportTo );
304
		}
305
	}
306
307
	/**
308
	 * Update a single field given it's name and type and an array of
309
	 * current fields. MySQL version.
310
	 *
311
	 * @param string $tableName The table name.
312
	 * @param string $name The field name.
313
	 * @param string $type The field type and attributes.
314
	 * @param array $currentFields List of fields as they have been found in the database.
315
	 * @param DatabaseBase|Database $db
316
	 * @param object $reportTo Object to report back to.
317
	 * @param string $position
318
	 */
319 2
	private static function updateMySqlField( $tableName, $name, $type, array $currentFields, $db, $reportTo, $position ) {
320 2
		if ( !array_key_exists( $name, $currentFields ) ) {
321
			self::reportProgress( "   ... creating field $name ... ", $reportTo );
322
323
			$db->query( "ALTER TABLE $tableName ADD `$name` $type $position", __METHOD__ );
324
			$result[$name] = 'new';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
325
326
			self::reportProgress( "done.\n", $reportTo );
327 2
		} elseif ( $currentFields[$name] != $type ) {
328
			self::reportProgress( "   ... changing type of field $name from '$currentFields[$name]' to '$type' ... ", $reportTo );
329
330
			// To avoid Error: 1068 Multiple primary key defined when a PRIMARY is involved
331
			if ( strpos( $type, 'AUTO_INCREMENT' ) !== false ) {
332
				$db->query( "ALTER TABLE $tableName DROP PRIMARY KEY", __METHOD__ );
333
			}
334
335
			$db->query( "ALTER TABLE $tableName CHANGE `$name` `$name` $type $position", __METHOD__ );
336
			$result[$name] = 'up';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
337
			self::reportProgress( "done.\n", $reportTo );
338
		} else {
339 2
			self::reportProgress( "   ... field $name is fine.\n", $reportTo );
340
		}
341 2
	}
342
343
	/**
344
	 * Make sure that each of the column descriptions in the given array is
345
	 * indexed by *one* index in the given DB table.
346
	 *
347
	 * @param string $tableName table name. Does not need to have been passed to DatabaseBase->tableName yet.
0 ignored issues
show
Documentation introduced by
There is no parameter named $tableName. Did you maybe mean $rawTableName?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
348
	 * @param array $indexes array of strings, each a comma separated list with column names to index
349
	 * @param DatabaseBase|Database $db DatabaseBase or Database
350
	 * @param object $reportTo object to report messages to; since 1.8
351
	 */
352 3
	public static function setupIndex( $rawTableName, array $indexes, $db, $reportTo = null ) {
353 3
		global $wgDBtype;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
354
355 3
		$tableName = $wgDBtype == 'postgres' ? $db->tableName( $rawTableName, 'raw' ) : $db->tableName( $rawTableName );
356
357 3
		self::reportProgress( "Checking index structures for table $tableName ...\n", $reportTo );
0 ignored issues
show
Bug introduced by
It seems like $reportTo defined by parameter $reportTo on line 352 can also be of type null; however, SMWSQLHelpers::reportProgress() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
358
359
		// First remove obsolete indexes.
360 3
		$oldIndexes = self::getIndexInfo( $db, $tableName );
361 3
		if ( $wgDBtype == 'sqlite' ) { // SQLite
362
			/// TODO We do not currently get the right column definitions in
363
			/// SQLLite; hence we can only drop all indexes. Wasteful.
364
			foreach ( $oldIndexes as $key => $index ) {
0 ignored issues
show
Bug introduced by
The expression $oldIndexes of type false|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
365
				self::dropIndex( $db, $key, $tableName, $key, $reportTo );
366
			}
367
		} else {
368 3
			foreach ( $oldIndexes as $key => $indexColumn ) {
0 ignored issues
show
Bug introduced by
The expression $oldIndexes of type false|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
369 3
				$id = array_search( $indexColumn, $indexes );
370 3
				if ( $id !== false || $key == 'PRIMARY' ) {
371 3
					self::reportProgress( "   ... index $indexColumn is fine.\n", $reportTo );
0 ignored issues
show
Bug introduced by
It seems like $reportTo defined by parameter $reportTo on line 352 can also be of type null; however, SMWSQLHelpers::reportProgress() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
372 3
					unset( $indexes[$id] );
373
				} else { // Duplicate or unrequired index.
374 3
					self::dropIndex( $db, $key, $tableName, $indexColumn, $reportTo );
375
				}
376
			}
377
		}
378
379
		// Add new indexes.
380 3
		foreach ( $indexes as $key => $index ) {
381
			// If the index is an array, it contains the column
382
			// name as first element, and index type as second one.
383 3
			if ( is_array( $index ) ) {
384 3
				$columns = $index[0];
385 3
				$type = count( $index ) > 1 ? $index[1] : 'INDEX';
386
			} else {
387 2
				$columns = $index;
388 2
				$type = 'INDEX';
389
			}
390
391 3
			self::createIndex( $db, $type, "{$tableName}_index{$key}", $tableName, $columns, $reportTo );
392
		}
393
394 3
		self::reportProgress( "   ... done.\n", $reportTo );
0 ignored issues
show
Bug introduced by
It seems like $reportTo defined by parameter $reportTo on line 352 can also be of type null; however, SMWSQLHelpers::reportProgress() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
395
396 3
		return true;
397
	}
398
399
	/**
400
	 * Get the information about all indexes of a table. The result is an
401
	 * array of format indexname => indexcolumns. The latter is a comma
402
	 * separated list.
403
	 *
404
	 * @since 1.8
405
	 * @param DatabaseBase|Database $db database handler
406
	 * @param string $tableName name of table
407
	 * @return array indexname => columns
408
	 */
409 3
	private static function getIndexInfo( $db, $tableName ) {
410 3
		global $wgDBtype;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
411
412 3
		$indexes = array();
413 3
		if ( $wgDBtype == 'postgres' ) { // postgresql
414
			$sql = "SELECT  i.relname AS indexname,"
415
				. " pg_get_indexdef(i.oid) AS indexdef, "
416
				. " replace(substring(pg_get_indexdef(i.oid) from E'\\\\((.*)\\\\)'), ' ' , '') AS indexcolumns"
417
				. " FROM pg_index x"
418
				. " JOIN pg_class c ON c.oid = x.indrelid"
419
				. " JOIN pg_class i ON i.oid = x.indexrelid"
420
				. " LEFT JOIN pg_namespace n ON n.oid = c.relnamespace"
421
				. " LEFT JOIN pg_tablespace t ON t.oid = i.reltablespace"
422
				. " WHERE c.relkind = 'r'::\"char\" AND i.relkind = 'i'::\"char\""
423
				. " AND c.relname = '" . $tableName . "'"
424
				. " AND NOT pg_get_indexdef(i.oid) ~ '^CREATE UNIQUE INDEX'";
425
			$res = $db->query( $sql, __METHOD__ );
426
427
			if ( !$res ) {
428
				return false;
429
			}
430
431
			foreach ( $res as $row ) {
432
				$indexes[$row->indexname] = $row->indexcolumns;
433
			}
434 3
		} elseif ( $wgDBtype == 'sqlite' ) { // SQLite
435
			$res = $db->query( 'PRAGMA index_list(' . $tableName . ')', __METHOD__ );
436
437
			if ( !$res ) {
438
				return false;
439
			}
440
441
			foreach ( $res as $row ) {
442
				/// FIXME The value should not be $row->name below?!
443
				if ( !array_key_exists( $row->name, $indexes ) ) {
444
					$indexes[$row->name] = $row->name;
445
				} else {
446
					$indexes[$row->name] .= ',' . $row->name;
447
				}
448
			}
449
		} else { // MySQL and default
450 3
			$res = $db->query( 'SHOW INDEX FROM ' . $tableName, __METHOD__ );
451
452 3
			if ( !$res ) {
453
				return false;
454
			}
455
456 3
			foreach ( $res as $row ) {
457 3
				if ( !array_key_exists( $row->Key_name, $indexes ) ) {
458 3
					$indexes[$row->Key_name] = $row->Column_name;
459
				} else {
460 3
					$indexes[$row->Key_name] .= ',' . $row->Column_name;
461
				}
462
			}
463
		}
464
465 3
		return $indexes;
466
	}
467
468
	/**
469
	 * Drop an index using the suitable SQL for various RDBMS.
470
	 *
471
	 * @since 1.8
472
	 * @param DatabaseBase|Database $db database handler
473
	 * @param string $indexName name fo the index as in DB
474
	 * @param string $tableName name of the table (not relevant in all DBMSs)
475
	 * @param string $columns list of column names to index, comma
476
	 * separated; only for reporting
477
	 * @param object $reportTo to report messages to
478
	 */
479 2
	private static function dropIndex( $db, $indexName, $tableName, $columns, $reportTo = null ) {
480 2
		global $wgDBtype;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
481
482 2
		self::reportProgress( "   ... removing index $columns ...", $reportTo );
0 ignored issues
show
Bug introduced by
It seems like $reportTo defined by parameter $reportTo on line 479 can also be of type null; however, SMWSQLHelpers::reportProgress() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
483 2
		if ( $wgDBtype == 'postgres' ) { // postgresql
484
			$db->query( 'DROP INDEX IF EXISTS ' . $indexName, __METHOD__ );
485 2
		} elseif ( $wgDBtype == 'sqlite' ) { // SQLite
486
			$db->query( 'DROP INDEX ' . $indexName, __METHOD__ );
487
		} else { // MySQL and default
488 2
			$db->query( 'DROP INDEX ' . $indexName . ' ON ' . $tableName, __METHOD__ );
489
		}
490 2
		self::reportProgress( "done.\n", $reportTo );
0 ignored issues
show
Bug introduced by
It seems like $reportTo defined by parameter $reportTo on line 479 can also be of type null; however, SMWSQLHelpers::reportProgress() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
491 2
	}
492
493
	/**
494
	 * Create an index using the suitable SQL for various RDBMS.
495
	 *
496
	 * @since 1.8
497
	 * @param DatabaseBase|Database $db Database handler
498
	 * @param string $type "INDEX", "UNIQUE" or similar
499
	 * @param string $indexName name fo the index as in DB
500
	 * @param string $tableName name of the table
501
	 * @param array $columns list of column names to index, comma separated
502
	 * @param object $reportTo object to report messages to
503
	 */
504 3
	private static function createIndex( $db, $type, $indexName, $tableName, $columns, $reportTo = null ) {
505 3
		global $wgDBtype;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
506
507 3
		self::reportProgress( "   ... creating new index $columns ...", $reportTo );
0 ignored issues
show
Bug introduced by
It seems like $reportTo defined by parameter $reportTo on line 504 can also be of type null; however, SMWSQLHelpers::reportProgress() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
508 3
		if ( $wgDBtype == 'postgres' ) { // postgresql
509
			if ( $db->indexInfo( $tableName, $indexName ) === false ) {
510
				$db->query( "CREATE $type $indexName ON $tableName ($columns)", __METHOD__ );
511
			}
512 3
		} elseif ( $wgDBtype == 'sqlite' ) { // SQLite
513
			$db->query( "CREATE $type $indexName ON $tableName ($columns)", __METHOD__ );
514
		} else { // MySQL and default
515 3
			$db->query( "ALTER TABLE $tableName ADD $type ($columns)", __METHOD__ );
516
		}
517 3
		self::reportProgress( "done.\n", $reportTo );
0 ignored issues
show
Bug introduced by
It seems like $reportTo defined by parameter $reportTo on line 504 can also be of type null; however, SMWSQLHelpers::reportProgress() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
518 3
	}
519
520
	/**
521
	 * Reports the given message to the reportProgress method of the
522
	 * $receiver.
523
	 *
524
	 * @param string $msg
525
	 * @param object $receiver
526
	 */
527 3
	private static function reportProgress( $msg, $receiver ) {
528 3
		if ( !is_null( $receiver ) ) {
529
			$receiver->reportProgress( $msg );
530
		}
531 3
	}
532
533
}
534