Index: trunk/extensions/SemanticMediaWiki/includes/storage/SMW_SQLHelpers.php |
— | — | @@ -63,202 +63,252 @@ |
64 | 64 | * Only if the type of some field changes will its order be adjusted explicitly. |
65 | 65 | * |
66 | 66 | * @param string $tableName The table name. Does not need to have been passed to DatabaseBase->tableName yet. |
67 | | - * @param array $columns The field names to put indexes on |
| 67 | + * @param array $columns The fields and their types the table should have. |
68 | 68 | * @param DatabaseBase $db |
69 | | - * @param $reportTo |
70 | | - * |
71 | | - * TODO: split up megamoth srysly o_O |
| 69 | + * @param $reportTo Object to report back to. |
72 | 70 | */ |
73 | 71 | public static function setupTable( $tableName, array $fields, DatabaseBase $db, $reportTo = null ) { |
74 | 72 | global $wgDBname, $wgDBtype, $wgDBTableOptions; |
75 | 73 | |
76 | 74 | $tableName = $db->tableName( $tableName ); |
77 | 75 | |
78 | | - self::reportProgress( "Setting up table $tableName ...\n", $reportTo ); |
| 76 | + self::reportProgress( "Checking table $tableName ...\n", $reportTo ); |
79 | 77 | |
80 | 78 | if ( $db->tableExists( $tableName ) === false ) { // create new table |
81 | | - $this->createTable( $tableName, $fields, $db, $reportTo ); |
82 | | - } else { // check table signature |
83 | | - self::reportProgress( " ... table exists already, checking structure ...\n", $reportTo ); |
84 | | - |
85 | | - if ( $wgDBtype == 'postgres' ) { // postgresql |
86 | | - // use the data dictionary in postgresql to get an output comparable to DESCRIBE |
87 | | - // To find out what kind of magic takes place here (and to remove the bugs included), simply use: |
88 | | - // psql |
89 | | - // \set ECHO_HIDDEN |
90 | | - // \d <tablename> |
91 | | - $sql = 'SELECT a.attname as "Field", ' |
92 | | - . ' upper(pg_catalog.format_type(a.atttypid, a.atttypmod)) as "Type", ' |
93 | | - . ' (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128) ' |
94 | | - . ' FROM pg_catalog.pg_attrdef d ' |
95 | | - . ' WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) as "Extra", ' |
96 | | - . ' case when a.attnotnull THEN \'NO\'::text else \'YES\'::text END as "Null", a.attnum ' |
97 | | - . ' FROM pg_catalog.pg_attribute a ' |
98 | | - . ' WHERE a.attrelid = (' |
99 | | - . ' SELECT c.oid ' |
100 | | - . ' FROM pg_catalog.pg_class c ' |
101 | | - . ' LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace ' |
102 | | - . ' WHERE c.relname ~ \'^(' . $tableName . ')$\' ' |
103 | | - . ' AND pg_catalog.pg_table_is_visible(c.oid) ' |
104 | | - . ' LIMIT 1 ' |
105 | | - . ' ) AND a.attnum > 0 AND NOT a.attisdropped ' |
106 | | - . ' ORDER BY a.attnum'; |
107 | | - } else { // MySQL |
108 | | - $sql = 'DESCRIBE ' . $tableName; |
| 79 | + self::reportProgress( " Table not found, now creating...\n", $reportTo ); |
| 80 | + self::createTable( $tableName, $fields, $db, $reportTo ); |
| 81 | + self::reportProgress( " ... done.\n", $reportTo ); |
| 82 | + } else { |
| 83 | + self::reportProgress( " Table already exists, checking structure ...\n", $reportTo ); |
| 84 | + self::updateTable( $tableName, $fields, $db, $reportTo ); |
| 85 | + self::reportProgress( " ... done.\n", $reportTo ); |
| 86 | + } |
| 87 | + } |
| 88 | + |
| 89 | + /** |
| 90 | + * Creates a new database table with the specified fields. |
| 91 | + * |
| 92 | + * @param string $tableName The table name. |
| 93 | + * @param array $columns The fields and their types the table should have. |
| 94 | + * @param DatabaseBase $db |
| 95 | + * @param $reportTo Object to report back to. |
| 96 | + */ |
| 97 | + protected static function createTable( $tableName, array $fields, DatabaseBase $db, $reportTo ) { |
| 98 | + global $wgDBtype, $wgDBTableOptions, $wgDBname; |
| 99 | + |
| 100 | + $sql = 'CREATE TABLE ' . ( $wgDBtype == 'postgres' ? '' : "`$wgDBname`." ) . $tableName . ' ('; |
| 101 | + |
| 102 | + $fieldSql = array(); |
| 103 | + |
| 104 | + foreach ( $fields as $fieldName => $fieldType ) { |
| 105 | + $fieldSql[] = "$fieldName $fieldType"; |
| 106 | + } |
| 107 | + |
| 108 | + $sql .= implode( ',', $fieldSql ) . ') '; |
| 109 | + if ( $wgDBtype != 'postgres' ) $sql .= $wgDBTableOptions; |
| 110 | + |
| 111 | + $db->query( $sql, __METHOD__ ); |
| 112 | + } |
| 113 | + |
| 114 | + /** |
| 115 | + * Update a table given an array of field names and field types. |
| 116 | + * |
| 117 | + * @param string $tableName The table name. |
| 118 | + * @param array $columns The fields and their types the table should have. |
| 119 | + * @param DatabaseBase $db |
| 120 | + * @param $reportTo Object to report back to. |
| 121 | + */ |
| 122 | + protected static function updateTable( $tableName, array $fields, DatabaseBase $db, $reportTo ) { |
| 123 | + global $wgDBtype; |
| 124 | + |
| 125 | + $currentFields = self::getFields( $tableName, $db, $reportTo ); |
| 126 | + |
| 127 | + $isPostgres = $wgDBtype == 'postgres'; |
| 128 | + |
| 129 | + if ( !$isPostgres ) $position = 'FIRST'; |
| 130 | + |
| 131 | + // Loop through all the field definitions, and handle each definition for either postgres or MySQL. |
| 132 | + foreach ( $fields as $fieldName => $fieldType ) { |
| 133 | + if ( $isPostgres ) { |
| 134 | + self::updatePostgresField( $tableName, $fieldName, $fieldType, $currentFields, $db, $reportTo ); |
109 | 135 | } |
| 136 | + else { |
| 137 | + self::updateMySqlField( $tableName, $fieldName, $fieldType, $currentFields, $db, $reportTo, $position ); |
| 138 | + $position = "AFTER $fieldName"; |
| 139 | + } |
110 | 140 | |
111 | | - $res = $db->query( $sql, __METHOD__ ); |
112 | | - $curfields = array(); |
113 | | - $result = array(); |
114 | | - |
115 | | - while ( $row = $db->fetchObject( $res ) ) { |
116 | | - $type = strtoupper( $row->Type ); |
| 141 | + $currentFields[$fieldName] = false; |
| 142 | + } |
| 143 | + |
| 144 | + // The updated fields have their value set to false, so if a field has a value |
| 145 | + // that differs from false, it's an obsolete one that should be removed. |
| 146 | + foreach ( $currentFields as $fieldName => $value ) { |
| 147 | + if ( $value !== false ) { |
| 148 | + SMWSQLHelpers::reportProgress( " ... deleting obsolete field $fieldName ... ", $reportTo ); |
117 | 149 | |
118 | | - if ( $wgDBtype == 'postgres' ) { // postgresql |
119 | | - if ( eregi( '^nextval\\(.+\\)$', $row->Extra ) ) { |
120 | | - $type = 'SERIAL NOT NULL'; |
121 | | - } elseif ( $row->Null != 'YES' ) { |
122 | | - $type .= ' NOT NULL'; |
123 | | - } |
124 | | - } else { // mysql |
125 | | - if ( substr( $type, 0, 8 ) == 'VARCHAR(' ) { |
126 | | - $type .= ' binary'; // just assume this to be the case for VARCHAR, though DESCRIBE will not tell us |
127 | | - } |
128 | | - |
129 | | - if ( $row->Null != 'YES' ) { |
130 | | - $type .= ' NOT NULL'; |
131 | | - } |
132 | | - |
133 | | - if ( $row->Key == 'PRI' ) { /// FIXME: updating "KEY" is not possible, the below query will fail in this case. |
134 | | - $type .= ' KEY'; |
135 | | - } |
136 | | - |
137 | | - if ( $row->Extra == 'auto_increment' ) { |
138 | | - $type .= ' AUTO_INCREMENT'; |
139 | | - } |
| 150 | + if ( $isPostgres ) { |
| 151 | + $db->query( "ALTER TABLE \"" . $tableName . "\" DROP COLUMN \"" . $fieldName . "\"", __METHOD__ ); |
140 | 152 | } |
| 153 | + else { |
| 154 | + $db->query( "ALTER TABLE $tableName DROP COLUMN `$fieldName`", __METHOD__ ); |
| 155 | + } |
141 | 156 | |
142 | | - $curfields[$row->Field] = $type; |
| 157 | + SMWSQLHelpers::reportProgress( "done.\n", $reportTo ); |
143 | 158 | } |
| 159 | + } |
| 160 | + } |
144 | 161 | |
| 162 | + /** |
| 163 | + * Returns an array of fields (as keys) and their types (as values). |
| 164 | + * |
| 165 | + * @param string $tableName The table name. |
| 166 | + * @param DatabaseBase $db |
| 167 | + * @param $reportTo Object to report back to. |
| 168 | + * |
| 169 | + * @return array |
| 170 | + */ |
| 171 | + protected static function getFields( $tableName, DatabaseBase $db, $reportTo ) { |
| 172 | + global $wgDBtype; |
| 173 | + |
| 174 | + if ( $wgDBtype == 'postgres' ) { |
| 175 | + // Use the data dictionary in postgresql to get an output comparable to DESCRIBE. |
| 176 | + $sql = <<<EOT |
| 177 | +SELECT |
| 178 | + a.attname as "Field", |
| 179 | + upper(pg_catalog.format_type(a.atttypid, a.atttypmod)) as "Type", |
| 180 | + (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128) |
| 181 | + FROM pg_catalog.pg_attrdef d |
| 182 | + WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) as "Extra", |
| 183 | + case when a.attnotnull THEN \'NO\'::text else \'YES\'::text END as "Null", a.attnum |
| 184 | + FROM pg_catalog.pg_attribute a |
| 185 | + WHERE a.attrelid = ( |
| 186 | + SELECT c.oid |
| 187 | + FROM pg_catalog.pg_class c |
| 188 | + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace |
| 189 | + WHERE c.relname ~ \'^(' . $tableName . ')$\' |
| 190 | + AND pg_catalog.pg_table_is_visible(c.oid) |
| 191 | + LIMIT 1 |
| 192 | + ) AND a.attnum > 0 AND NOT a.attisdropped |
| 193 | + ORDER BY a.attnum |
| 194 | +EOT; |
| 195 | + } else { // MySQL |
| 196 | + $sql = 'DESCRIBE ' . $tableName; |
| 197 | + } |
| 198 | + |
| 199 | + $res = $db->query( $sql, __METHOD__ ); |
| 200 | + $curfields = array(); |
| 201 | + $result = array(); |
| 202 | + |
| 203 | + while ( $row = $db->fetchObject( $res ) ) { |
| 204 | + $type = strtoupper( $row->Type ); |
| 205 | + |
145 | 206 | if ( $wgDBtype == 'postgres' ) { // postgresql |
146 | | - foreach ( $fields as $name => $type ) { |
147 | | - $keypos = strpos( $type, ' PRIMARY KEY' ); |
148 | | - |
149 | | - if ( $keypos > 0 ) { |
150 | | - $type = substr( $type, 0, $keypos ); |
151 | | - } |
152 | | - |
153 | | - if ( !array_key_exists( $name, $curfields ) ) { |
154 | | - SMWSQLHelpers::reportProgress( " ... creating column $name ... ", $reportTo ); |
155 | | - |
156 | | - $db->query( "ALTER TABLE $tableName ADD \"" . $name . "\" $type", __METHOD__ ); |
157 | | - $result[$name] = 'new'; |
158 | | - |
159 | | - SMWSQLHelpers::reportProgress( "done \n", $reportTo ); |
160 | | - } elseif ( $curfields[$name] != $type ) { |
161 | | - SMWSQLHelpers::reportProgress( " ... changing type of column $name from '$curfields[$name]' to '$type' ... ", $reportTo ); |
162 | | - $notnullposnew = strpos( $type, ' NOT NULL' ); |
163 | | - |
164 | | - if ( $notnullposnew > 0 ) { |
165 | | - $type = substr( $type, 0, $notnullposnew ); |
166 | | - } |
167 | | - |
168 | | - $notnullposold = strpos( $curfields[$name], ' NOT NULL' ); |
169 | | - $typeold = ( $notnullposold > 0 ) ? substr( $curfields[$name], 0, $notnullposold ):$curfields[$name]; |
170 | | - |
171 | | - if ( $typeold != $type ) { |
172 | | - $db->query( "ALTER TABLE \"" . $tableName . "\" ALTER COLUMN \"" . $name . "\" TYPE " . $type, __METHOD__ ); |
173 | | - } |
174 | | - |
175 | | - if ( $notnullposold != $notnullposnew ) { |
176 | | - $db->query( "ALTER TABLE \"" . $tableName . "\" ALTER COLUMN \"" . $name . "\" " . ( $notnullposnew > 0 ? 'SET':'DROP' ) . " NOT NULL", __METHOD__ ); |
177 | | - } |
178 | | - |
179 | | - $result[$name] = 'up'; |
180 | | - $curfields[$name] = false; |
181 | | - SMWSQLHelpers::reportProgress( "done.\n", $reportTo ); |
182 | | - } else { |
183 | | - SMWSQLHelpers::reportProgress( " ... column $name is fine\n", $reportTo ); |
184 | | - $curfields[$name] = false; |
185 | | - } |
| 207 | + if ( eregi( '^nextval\\(.+\\)$', $row->Extra ) ) { |
| 208 | + $type = 'SERIAL NOT NULL'; |
| 209 | + } elseif ( $row->Null != 'YES' ) { |
| 210 | + $type .= ' NOT NULL'; |
186 | 211 | } |
| 212 | + } else { // mysql |
| 213 | + if ( substr( $type, 0, 8 ) == 'VARCHAR(' ) { |
| 214 | + $type .= ' binary'; // just assume this to be the case for VARCHAR, though DESCRIBE will not tell us |
| 215 | + } |
187 | 216 | |
188 | | - foreach ( $curfields as $name => $value ) { |
189 | | - if ( $value !== false ) { |
190 | | - SMWSQLHelpers::reportProgress( " ... deleting obsolete column $name ... ", $reportTo ); |
191 | | - |
192 | | - $db->query( "ALTER TABLE \"" . $tableName . "\" DROP COLUMN \"" . $name . "\"", __METHOD__ ); |
193 | | - $result[$name] = 'del'; |
194 | | - |
195 | | - SMWSQLHelpers::reportProgress( "done.\n", $reportTo ); |
196 | | - } |
| 217 | + if ( $row->Null != 'YES' ) { |
| 218 | + $type .= ' NOT NULL'; |
197 | 219 | } |
198 | | - } else { // mysql |
199 | | - $position = 'FIRST'; |
200 | | - |
201 | | - foreach ( $fields as $name => $type ) { |
202 | | - if ( !array_key_exists( $name, $curfields ) ) { |
203 | | - SMWSQLHelpers::reportProgress( " ... creating column $name ... ", $reportTo ); |
204 | | - |
205 | | - $db->query( "ALTER TABLE $tableName ADD `$name` $type $position", __METHOD__ ); |
206 | | - $result[$name] = 'new'; |
207 | | - |
208 | | - SMWSQLHelpers::reportProgress( "done \n", $reportTo ); |
209 | | - } elseif ( $curfields[$name] != $type ) { |
210 | | - SMWSQLHelpers::reportProgress( " ... changing type of column $name from '$curfields[$name]' to '$type' ... ", $reportTo ); |
211 | | - |
212 | | - $db->query( "ALTER TABLE $tableName CHANGE `$name` `$name` $type $position", __METHOD__ ); |
213 | | - $result[$name] = 'up'; |
214 | | - $curfields[$name] = false; |
215 | | - |
216 | | - SMWSQLHelpers::reportProgress( "done.\n", $reportTo ); |
217 | | - } else { |
218 | | - SMWSQLHelpers::reportProgress( " ... column $name is fine\n", $reportTo ); |
219 | | - |
220 | | - $curfields[$name] = false; |
| 220 | + |
| 221 | + if ( $row->Key == 'PRI' ) { /// FIXME: updating "KEY" is not possible, the below query will fail in this case. |
| 222 | + $type .= ' KEY'; |
221 | 223 | } |
222 | | - $position = "AFTER $name"; |
223 | | - } |
224 | | - |
225 | | - foreach ( $curfields as $name => $value ) { |
226 | | - if ( $value !== false ) { // not encountered yet --> delete |
227 | | - SMWSQLHelpers::reportProgress( " ... deleting obsolete column $name ... ", $reportTo ); |
228 | | - $db->query( "ALTER TABLE $tableName DROP COLUMN `$name`", __METHOD__ ); |
229 | | - $result[$name] = 'del'; |
230 | | - SMWSQLHelpers::reportProgress( "done.\n", $reportTo ); |
| 224 | + |
| 225 | + if ( $row->Extra == 'auto_increment' ) { |
| 226 | + $type .= ' AUTO_INCREMENT'; |
231 | 227 | } |
232 | | - } |
233 | 228 | } |
234 | | - |
235 | | - SMWSQLHelpers::reportProgress( " ... table $tableName set up successfully.\n", $reportTo ); |
236 | 229 | |
237 | | - return $result; |
| 230 | + $curfields[$row->Field] = $type; |
238 | 231 | } |
| 232 | + |
| 233 | + return $curfields; |
239 | 234 | } |
240 | 235 | |
241 | | - private static function createTable( $tableName, array $fields, DatabaseBase $db, $reportTo ) { |
242 | | - global $wgDBtype, $wgDBTableOptions, $wgDBname; |
| 236 | + /** |
| 237 | + * Update a single field given it's name and type and an array of current fields. Postgres version. |
| 238 | + * |
| 239 | + * @param string $tableName The table name. |
| 240 | + * @param string $name The field name. |
| 241 | + * @param string $type The field type and attributes. |
| 242 | + * @param array $currentFields List of fields as they have been found in the database. |
| 243 | + * @param DatabaseBase $db |
| 244 | + * @param object $reportTo Object to report back to. |
| 245 | + */ |
| 246 | + protected static function updatePostgresField( $tableName, $name, $type, array $currentFields, DatabaseBase $db, $reportTo ) { |
| 247 | + $keypos = strpos( $type, ' PRIMARY KEY' ); |
243 | 248 | |
244 | | - $sql = 'CREATE TABLE ' . ( $wgDBtype == 'postgres' ? '' : "`$wgDBname`." ) . $tableName . ' ('; |
| 249 | + if ( $keypos > 0 ) { |
| 250 | + $type = substr( $type, 0, $keypos ); |
| 251 | + } |
245 | 252 | |
246 | | - $fieldSql = array(); |
247 | | - |
248 | | - foreach ( $fields as $fieldName => $fieldType ) { |
249 | | - $fieldSql[] = "$fieldName $fieldType"; |
| 253 | + if ( !array_key_exists( $name, $currentFields ) ) { |
| 254 | + self::reportProgress( " ... creating field $name ... ", $reportTo ); |
| 255 | + |
| 256 | + $db->query( "ALTER TABLE $tableName ADD \"" . $name . "\" $type", __METHOD__ ); |
| 257 | + |
| 258 | + self::reportProgress( "done.\n", $reportTo ); |
| 259 | + } elseif ( $currentFields[$name] != $type ) { |
| 260 | + self::reportProgress( " ... changing type of field $name from '$currentFields[$name]' to '$type' ... ", $reportTo ); |
| 261 | + $notnullposnew = strpos( $type, ' NOT NULL' ); |
| 262 | + |
| 263 | + if ( $notnullposnew > 0 ) { |
| 264 | + $type = substr( $type, 0, $notnullposnew ); |
| 265 | + } |
| 266 | + |
| 267 | + $notnullposold = strpos( $currentFields[$name], ' NOT NULL' ); |
| 268 | + $typeold = ( $notnullposold > 0 ) ? substr( $currentFields[$name], 0, $notnullposold ) : $currentFields[$name]; |
| 269 | + |
| 270 | + if ( $typeold != $type ) { |
| 271 | + $db->query( "ALTER TABLE \"" . $tableName . "\" ALTER COLUMN \"" . $name . "\" TYPE " . $type, __METHOD__ ); |
| 272 | + } |
| 273 | + |
| 274 | + if ( $notnullposold != $notnullposnew ) { |
| 275 | + $db->query( "ALTER TABLE \"" . $tableName . "\" ALTER COLUMN \"" . $name . "\" " . ( $notnullposnew > 0 ? 'SET' : 'DROP' ) . " NOT NULL", __METHOD__ ); |
| 276 | + } |
| 277 | + |
| 278 | + self::reportProgress( "done.\n", $reportTo ); |
| 279 | + } else { |
| 280 | + self::reportProgress( " ... field $name is fine.\n", $reportTo ); |
250 | 281 | } |
251 | | - |
252 | | - $sql .= implode( ',', $fieldSql ) . ') '; |
253 | | - if ( $wgDBtype != 'postgres' ) $sql .= $wgDBTableOptions; |
254 | | - |
255 | | - $db->query( $sql, __METHOD__ ); |
256 | | - |
257 | | - self::reportProgress( " ... new table created\n", $reportTo ); |
258 | 282 | } |
259 | 283 | |
260 | | - private static function updateTable() { |
261 | | - |
262 | | - } |
| 284 | + /** |
| 285 | + * Update a single field given it's name and type and an array of current fields. MySQL version. |
| 286 | + * |
| 287 | + * @param string $tableName The table name. |
| 288 | + * @param string $name The field name. |
| 289 | + * @param string $type The field type and attributes. |
| 290 | + * @param array $currentFields List of fields as they have been found in the database. |
| 291 | + * @param DatabaseBase $db |
| 292 | + * @param object $reportTo Object to report back to. |
| 293 | + * @param string $position |
| 294 | + */ |
| 295 | + protected static function updateMySqlField( $tableName, $name, $type, array $currentFields, DatabaseBase $db, $reportTo, $position ) { |
| 296 | + if ( !array_key_exists( $name, $currentFields ) ) { |
| 297 | + self::reportProgress( " ... creating field $name ... ", $reportTo ); |
| 298 | + |
| 299 | + $db->query( "ALTER TABLE $tableName ADD `$name` $type $position", __METHOD__ ); |
| 300 | + $result[$name] = 'new'; |
| 301 | + |
| 302 | + self::reportProgress( "done.\n", $reportTo ); |
| 303 | + } elseif ( $currentFields[$name] != $type ) { |
| 304 | + self::reportProgress( " ... changing type of field $name from '$currentFields[$name]' to '$type' ... ", $reportTo ); |
| 305 | + |
| 306 | + $db->query( "ALTER TABLE $tableName CHANGE `$name` `$name` $type $position", __METHOD__ ); |
| 307 | + $result[$name] = 'up'; |
| 308 | + self::reportProgress( "done.\n", $reportTo ); |
| 309 | + } else { |
| 310 | + self::reportProgress( " ... field $name is fine.\n", $reportTo ); |
| 311 | + } |
| 312 | + } |
263 | 313 | |
264 | 314 | /** |
265 | 315 | * Make sure that each of the column descriptions in the given array is indexed by *one* index |
— | — | @@ -342,8 +392,13 @@ |
343 | 393 | return true; |
344 | 394 | } |
345 | 395 | |
346 | | - /// If a receiver is given, report the given message to its reportProgress method. |
347 | | - protected static function reportProgress( $msg, $receiver = null ) { |
| 396 | + /** |
| 397 | + * Reports the given message to the reportProgress method of the $receiver. |
| 398 | + * |
| 399 | + * @param string $msg |
| 400 | + * @param object $receiver |
| 401 | + */ |
| 402 | + protected static function reportProgress( $msg, $receiver ) { |
348 | 403 | if ( $receiver !== null ) $receiver->reportProgress( $msg ); |
349 | 404 | } |
350 | 405 | |