1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace SMW\SQLStore\TableBuilder; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* @license GNU GPL v2+ |
7
|
|
|
* @since 2.5 |
8
|
|
|
* |
9
|
|
|
* @author mwjames |
10
|
|
|
*/ |
11
|
|
|
class MySQLTableBuilder extends TableBuilder { |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* @since 2.5 |
15
|
|
|
* |
16
|
|
|
* {@inheritDoc} |
17
|
|
|
*/ |
18
|
|
|
public function getStandardFieldType( $input ) { |
19
|
|
|
|
20
|
|
|
switch ( $input ) { |
21
|
|
|
case 'id': |
22
|
|
|
return 'INT(8) UNSIGNED'; // like page_id in MW page table |
23
|
|
|
case 'id primary': |
24
|
|
|
return 'INT(8) UNSIGNED' . ' NOT NULL KEY AUTO_INCREMENT'; // like page_id in MW page table |
25
|
|
|
case 'namespace': |
26
|
|
|
return 'INT(11)'; // like page_namespace in MW page table |
27
|
|
|
case 'title': |
28
|
|
|
return 'VARBINARY(255)'; // like page_title in MW page table |
29
|
|
|
case 'iw': |
30
|
|
|
return 'VARBINARY(32)'; // like iw_prefix in MW interwiki table |
31
|
|
|
case 'blob': |
32
|
|
|
return 'MEDIUMBLOB'; // larger blobs of character data, usually not subject to SELECT conditions |
33
|
|
|
case 'boolean': |
34
|
|
|
return 'TINYINT(1)'; |
35
|
|
|
case 'double': |
36
|
|
|
return 'DOUBLE'; |
37
|
|
|
case 'integer': |
38
|
|
|
return 'INT(8)'; |
39
|
|
|
case 'usage count': |
40
|
|
|
return 'INT(8) UNSIGNED'; |
41
|
|
|
case 'integer unsigned': |
42
|
|
|
return 'INT(8) UNSIGNED'; |
43
|
|
|
case 'sort': |
44
|
|
|
return 'VARCHAR(255)'; |
45
|
|
|
} |
46
|
|
|
|
47
|
|
|
return false; |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
/** Create */ |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* @since 2.5 |
54
|
|
|
* |
55
|
|
|
* {@inheritDoc} |
56
|
|
|
*/ |
57
|
|
|
protected function doCreateTable( $tableName, array $tableOptions = null ) { |
58
|
|
|
|
59
|
|
|
$tableName = $this->connection->tableName( $tableName ); |
60
|
|
|
|
61
|
|
|
$sql = 'CREATE TABLE ' . $this->getSQLFromDBName( $tableOptions ) . $tableName . ' ('; |
|
|
|
|
62
|
|
|
|
63
|
|
|
$fieldSql = array(); |
64
|
|
|
$fields = $tableOptions['fields']; |
65
|
|
|
|
66
|
|
|
foreach ( $fields as $fieldName => $fieldType ) { |
67
|
|
|
$fieldSql[] = "$fieldName $fieldType"; |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
$sql .= implode( ',', $fieldSql ) . ') '; |
71
|
|
|
$sql .= $this->getSQLFromDBTableOptions( $tableOptions ); |
72
|
|
|
|
73
|
|
|
$this->connection->query( $sql, __METHOD__ ); |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
private function getSQLFromDBName( array $tableOptions ) { |
|
|
|
|
77
|
|
|
global $wgDBname; |
|
|
|
|
78
|
|
|
return "`$wgDBname`."; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
private function getSQLFromDBTableOptions( array $tableOptions ) { |
82
|
|
|
|
83
|
|
|
if ( isset( $tableOptions['ftSearchOptions']['mysql'] ) ) { |
84
|
|
|
|
85
|
|
|
$tableOption = $tableOptions['ftSearchOptions']['mysql']; |
86
|
|
|
|
87
|
|
|
// By convention the first index has table specific relevance |
88
|
|
|
if ( is_array( $tableOption ) ) { |
89
|
|
|
$tableOption = isset( $tableOption[0] ) ? $tableOption[0] : ''; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
return $tableOption; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
// This replacement is needed for compatibility, see http://bugs.mysql.com/bug.php?id=17501 |
96
|
|
|
if ( isset( $tableOptions['wgDBTableOptions'] ) ) { |
97
|
|
|
return str_replace( 'TYPE', 'ENGINE', $tableOptions['wgDBTableOptions'] ); |
98
|
|
|
} |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
/** Update */ |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* @since 2.5 |
105
|
|
|
* |
106
|
|
|
* {@inheritDoc} |
107
|
|
|
*/ |
108
|
|
|
protected function doUpdateTable( $tableName, array $tableOptions = null ) { |
109
|
|
|
|
110
|
|
|
$tableName = $this->connection->tableName( $tableName ); |
111
|
|
|
$currentFields = $this->getCurrentFields( $tableName ); |
112
|
|
|
|
113
|
|
|
$fields = $tableOptions['fields']; |
114
|
|
|
$position = 'FIRST'; |
115
|
|
|
|
116
|
|
|
// Loop through all the field definitions, and handle each definition for either postgres or MySQL. |
117
|
|
|
foreach ( $fields as $fieldName => $fieldType ) { |
118
|
|
|
$this->doUpdateField( $tableName, $fieldName, $fieldType, $currentFields, $position, $tableOptions ); |
119
|
|
|
|
120
|
|
|
$position = "AFTER $fieldName"; |
121
|
|
|
$currentFields[$fieldName] = false; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
// The updated fields have their value set to false, so if a field has a value |
125
|
|
|
// that differs from false, it's an obsolete one that should be removed. |
126
|
|
|
foreach ( $currentFields as $fieldName => $value ) { |
127
|
|
|
if ( $value !== false ) { |
128
|
|
|
$this->doDropField( $tableName, $fieldName ); |
129
|
|
|
} |
130
|
|
|
} |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
private function getCurrentFields( $tableName ) { |
134
|
|
|
|
135
|
|
|
$sql = 'DESCRIBE ' . $tableName; |
136
|
|
|
|
137
|
|
|
$res = $this->connection->query( $sql, __METHOD__ ); |
138
|
|
|
$currentFields = array(); |
139
|
|
|
|
140
|
|
|
foreach ( $res as $row ) { |
141
|
|
|
$type = strtoupper( $row->Type ); |
142
|
|
|
|
143
|
|
|
if ( substr( $type, 0, 8 ) == 'VARCHAR(' ) { |
144
|
|
|
$type .= ' binary'; // just assume this to be the case for VARCHAR, though DESCRIBE will not tell us |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
if ( $row->Null != 'YES' ) { |
148
|
|
|
$type .= ' NOT NULL'; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
if ( $row->Key == 'PRI' ) { /// FIXME: updating "KEY" is not possible, the below query will fail in this case. |
152
|
|
|
$type .= ' KEY'; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
if ( $row->Extra == 'auto_increment' ) { |
156
|
|
|
$type .= ' AUTO_INCREMENT'; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
$currentFields[$row->Field] = $type; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
return $currentFields; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
private function doUpdateField( $tableName, $fieldName, $fieldType, $currentFields, $position, array $tableOptions ) { |
|
|
|
|
166
|
|
|
|
167
|
|
|
if ( !array_key_exists( $fieldName, $currentFields ) ) { |
168
|
|
|
$this->doCreateField( $tableName, $fieldName, $position, $fieldType ); |
169
|
|
|
} elseif ( $currentFields[$fieldName] != $fieldType ) { |
170
|
|
|
$this->doUpdateFieldType( $tableName, $fieldName, $position, $currentFields[$fieldName], $fieldType ); |
171
|
|
|
} else { |
172
|
|
|
$this->reportMessage( " ... field $fieldName is fine.\n" ); |
173
|
|
|
} |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
private function doCreateField( $tableName, $fieldName, $position, $fieldType ) { |
177
|
|
|
$this->reportMessage( " ... creating field $fieldName ... " ); |
178
|
|
|
$this->connection->query( "ALTER TABLE $tableName ADD `$fieldName` $fieldType $position", __METHOD__ ); |
179
|
|
|
$this->reportMessage( "done.\n" ); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
private function doUpdateFieldType( $tableName, $fieldName, $position, $oldFieldType, $newFieldType ) { |
183
|
|
|
$this->reportMessage( " ... changing type of field $fieldName from '$oldFieldType' to '$newFieldType' ... " ); |
184
|
|
|
|
185
|
|
|
// To avoid Error: 1068 Multiple primary key defined when a PRIMARY is involved |
186
|
|
|
if ( strpos( $newFieldType, 'AUTO_INCREMENT' ) !== false ) { |
187
|
|
|
$this->connection->query( "ALTER TABLE $tableName DROP PRIMARY KEY", __METHOD__ ); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
$this->connection->query( "ALTER TABLE $tableName CHANGE `$fieldName` `$fieldName` $newFieldType $position", __METHOD__ ); |
191
|
|
|
|
192
|
|
|
// http://stackoverflow.com/questions/1873085/how-to-convert-from-varbinary-to-char-varchar-in-mysql |
193
|
|
|
// http://bugs.mysql.com/bug.php?id=34564 |
194
|
|
|
if ( strpos( $oldFieldType, 'VARBINARY' ) !== false && strpos( $newFieldType, 'VARCHAR' ) !== false ) { |
|
|
|
|
195
|
|
|
// $this->connection->query( "SELECT CAST($fieldName AS CHAR) from $tableName", __METHOD__ ); |
|
|
|
|
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
$this->reportMessage( "done.\n" ); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
private function doDropField( $tableName, $fieldName ) { |
202
|
|
|
$this->reportMessage( " ... deleting obsolete field $fieldName ... " ); |
203
|
|
|
$this->connection->query( "ALTER TABLE $tableName DROP COLUMN `$fieldName`", __METHOD__ ); |
204
|
|
|
$this->reportMessage( "done.\n" ); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** Index */ |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* @since 2.5 |
211
|
|
|
* |
212
|
|
|
* {@inheritDoc} |
213
|
|
|
*/ |
214
|
|
|
protected function doCreateIndicies( $tableName, array $indexOptions = null ) { |
215
|
|
|
|
216
|
|
|
$indicies = $indexOptions['indicies']; |
217
|
|
|
|
218
|
|
|
// First remove possible obsolete indicies |
219
|
|
|
$this->doDropObsoleteIndicies( $tableName, $indicies ); |
220
|
|
|
|
221
|
|
|
// Add new indexes. |
222
|
|
|
foreach ( $indicies as $indexName => $index ) { |
223
|
|
|
// If the index is an array, it contains the column |
224
|
|
|
// name as first element, and index type as second one. |
225
|
|
|
if ( is_array( $index ) ) { |
226
|
|
|
$columns = $index[0]; |
227
|
|
|
$indexType = count( $index ) > 1 ? $index[1] : 'INDEX'; |
228
|
|
|
} else { |
229
|
|
|
$columns = $index; |
230
|
|
|
$indexType = 'INDEX'; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
$this->doCreateIndex( $tableName, $indexType, $indexName, $columns, $indexOptions ); |
234
|
|
|
} |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
private function doDropObsoleteIndicies( $tableName, array &$indicies ) { |
238
|
|
|
|
239
|
|
|
$tableName = $this->connection->tableName( $tableName ); |
240
|
|
|
$currentIndicies = $this->getIndexInfo( $tableName ); |
241
|
|
|
|
242
|
|
|
foreach ( $currentIndicies as $indexName => $indexColumn ) { |
243
|
|
|
$id = array_search( $indexColumn, $indicies ); |
244
|
|
|
if ( $id !== false || $indexName == 'PRIMARY' ) { |
245
|
|
|
$this->reportMessage( " ... index $indexColumn is fine.\n" ); |
246
|
|
|
|
247
|
|
|
if ( $id !== false ) { |
248
|
|
|
unset( $indicies[$id] ); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
} else { // Duplicate or unrequired index. |
252
|
|
|
$this->doDropIndex( $tableName, $indexName, $indexColumn ); |
253
|
|
|
} |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* Get the information about all indexes of a table. The result is an |
259
|
|
|
* array of format indexname => indexcolumns. The latter is a comma |
260
|
|
|
* separated list. |
261
|
|
|
* |
262
|
|
|
* @return array indexname => columns |
263
|
|
|
*/ |
264
|
|
|
private function getIndexInfo( $tableName ) { |
265
|
|
|
|
266
|
|
|
$indices = array(); |
267
|
|
|
|
268
|
|
|
$res = $this->connection->query( 'SHOW INDEX FROM ' . $tableName, __METHOD__ ); |
269
|
|
|
|
270
|
|
|
if ( !$res ) { |
271
|
|
|
return $indices; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
foreach ( $res as $row ) { |
275
|
|
|
if ( !array_key_exists( $row->Key_name, $indices ) ) { |
276
|
|
|
$indices[$row->Key_name] = $row->Column_name; |
277
|
|
|
} else { |
278
|
|
|
$indices[$row->Key_name] .= ',' . $row->Column_name; |
279
|
|
|
} |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
return $indices; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
private function doDropIndex( $tableName, $indexName, $columns ) { |
286
|
|
|
$this->reportMessage( " ... removing index $columns ..." ); |
287
|
|
|
$this->connection->query( 'DROP INDEX ' . $indexName . ' ON ' . $tableName, __METHOD__ ); |
288
|
|
|
$this->reportMessage( "done.\n" ); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
private function doCreateIndex( $tableName, $indexType, $indexName, $columns, array $indexOptions ) { |
|
|
|
|
292
|
|
|
|
293
|
|
|
$tableName = $this->connection->tableName( $tableName ); |
294
|
|
|
$indexOption = ''; |
295
|
|
|
|
296
|
|
|
$this->reportMessage( " ... creating new index $columns ..." ); |
297
|
|
|
|
298
|
|
|
if ( isset( $indexOptions['ftSearchOptions']['mysql'] ) ) { |
299
|
|
|
$indexOption = $indexOptions['ftSearchOptions']['mysql']; |
300
|
|
|
|
301
|
|
|
// By convention the second index has index specific relevance |
302
|
|
|
if ( is_array( $indexOption ) ) { |
303
|
|
|
$indexOption = isset( $indexOption[1] ) ? $indexOption[1] : ''; |
304
|
|
|
} |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
if ( $indexType === 'FULLTEXT' ) { |
308
|
|
|
$this->connection->query( "ALTER TABLE $tableName ADD $indexType $columns ($columns) $indexOption", __METHOD__ ); |
309
|
|
|
} else { |
310
|
|
|
$this->connection->query( "ALTER TABLE $tableName ADD $indexType ($columns)", __METHOD__ ); |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
$this->reportMessage( "done.\n" ); |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
/** Drop */ |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* @since 2.5 |
320
|
|
|
* |
321
|
|
|
* {@inheritDoc} |
322
|
|
|
*/ |
323
|
|
|
protected function doDropTable( $tableName ) { |
324
|
|
|
$this->connection->query( 'DROP TABLE ' . $this->connection->tableName( $tableName ), __METHOD__ ); |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
} |
328
|
|
|
|
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.