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 PostgresTableBuilder 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 'SERIAL'; // like page_id in MW page table |
23
|
|
|
case 'id primary': |
24
|
|
|
return 'SERIAL' . ' NOT NULL PRIMARY KEY'; // like page_id in MW page table |
25
|
|
|
case 'namespace': |
26
|
|
|
return 'BIGINT'; // like page_namespace in MW page table |
27
|
|
|
case 'title': |
28
|
|
|
return 'TEXT'; // like page_title in MW page table |
29
|
|
|
case 'iw': |
30
|
|
|
return 'TEXT'; // like iw_prefix in MW interwiki table |
31
|
|
|
case 'blob': |
32
|
|
|
return 'BYTEA'; // larger blobs of character data, usually not subject to SELECT conditions |
33
|
|
|
case 'boolean': |
34
|
|
|
return 'BOOLEAN'; |
35
|
|
|
case 'double': |
36
|
|
|
return 'DOUBLE PRECISION'; |
37
|
|
|
case 'integer': |
38
|
|
|
case 'usage count': |
39
|
|
|
return 'bigint'; |
40
|
|
|
case 'integer unsigned': |
41
|
|
|
return 'INTEGER'; |
42
|
|
|
case 'sort': |
43
|
|
|
return 'TEXT'; |
44
|
|
|
} |
45
|
|
|
|
46
|
|
|
return false; |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
/** Create */ |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @since 2.5 |
53
|
|
|
* |
54
|
|
|
* {@inheritDoc} |
55
|
|
|
*/ |
56
|
|
|
protected function doCreateTable( $tableName, array $tableOptions = null ) { |
57
|
|
|
|
58
|
|
|
$tableName = $this->connection->tableName( $tableName ); |
59
|
|
|
|
60
|
|
|
$fieldSql = array(); |
61
|
|
|
$fields = $tableOptions['fields']; |
62
|
|
|
|
63
|
|
|
foreach ( $fields as $fieldName => $fieldType ) { |
64
|
|
|
$fieldSql[] = "$fieldName $fieldType"; |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
$this->connection->query( 'CREATE TABLE ' . $tableName . ' (' . implode( ',', $fieldSql ) . ') ', __METHOD__ ); |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
/** Update */ |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* @since 2.5 |
74
|
|
|
* |
75
|
|
|
* {@inheritDoc} |
76
|
|
|
*/ |
77
|
|
|
protected function doUpdateTable( $tableName, array $tableOptions = null ) { |
78
|
|
|
|
79
|
|
|
$tableName = $this->connection->tableName( $tableName ); |
80
|
|
|
$currentFields = $this->getCurrentFields( $tableName ); |
81
|
|
|
|
82
|
|
|
$fields = $tableOptions['fields']; |
83
|
|
|
$position = 'FIRST'; |
84
|
|
|
|
85
|
|
|
// Loop through all the field definitions, and handle each definition for either postgres or MySQL. |
86
|
|
|
foreach ( $fields as $fieldName => $fieldType ) { |
87
|
|
|
$this->doUpdateField( $tableName, $fieldName, $fieldType, $currentFields, $position, $tableOptions ); |
88
|
|
|
|
89
|
|
|
$position = "AFTER $fieldName"; |
90
|
|
|
$currentFields[$fieldName] = false; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
// The updated fields have their value set to false, so if a field has a value |
94
|
|
|
// that differs from false, it's an obsolete one that should be removed. |
95
|
|
|
foreach ( $currentFields as $fieldName => $value ) { |
96
|
|
|
if ( $value !== false ) { |
97
|
|
|
$this->doDropField( $tableName, $fieldName ); |
98
|
|
|
} |
99
|
|
|
} |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
private function getCurrentFields( $tableName ) { |
103
|
|
|
|
104
|
|
|
$tableName = str_replace( '"', '', $tableName ); |
105
|
|
|
// Use the data dictionary in postgresql to get an output comparable to DESCRIBE. |
106
|
|
|
/* |
107
|
|
|
$sql = <<<EOT |
108
|
|
|
SELECT |
109
|
|
|
a.attname as "Field", |
110
|
|
|
upper(pg_catalog.format_type(a.atttypid, a.atttypmod)) as "Type", |
111
|
|
|
(SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128) |
112
|
|
|
FROM pg_catalog.pg_attrdef d |
113
|
|
|
WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) as "Extra", |
114
|
|
|
case when a.attnotnull THEN 'NO'::text else 'YES'::text END as "Null", a.attnum |
115
|
|
|
FROM pg_catalog.pg_attribute a |
116
|
|
|
WHERE a.attrelid = ( |
117
|
|
|
SELECT c.oid |
118
|
|
|
FROM pg_catalog.pg_class c |
119
|
|
|
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace |
120
|
|
|
WHERE c.relname ~ '^($tableName)$' |
121
|
|
|
AND pg_catalog.pg_table_is_visible(c.oid) |
122
|
|
|
LIMIT 1 |
123
|
|
|
) AND a.attnum > 0 AND NOT a.attisdropped |
124
|
|
|
ORDER BY a.attnum |
125
|
|
|
EOT; |
126
|
|
|
*/ |
127
|
|
|
|
128
|
|
|
$sql = "SELECT a.attname as \"Field\"," |
129
|
|
|
. " upper(pg_catalog.format_type(a.atttypid, a.atttypmod)) as \"Type\"," |
130
|
|
|
. " (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)" |
131
|
|
|
. " FROM pg_catalog.pg_attrdef d" |
132
|
|
|
. " WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) as \"Extra\", " |
133
|
|
|
. " case when a.attnotnull THEN 'NO'::text else 'YES'::text END as \"Null\", a.attnum" |
134
|
|
|
. " FROM pg_catalog.pg_attribute a" |
135
|
|
|
. " WHERE a.attrelid = (SELECT c.oid" |
136
|
|
|
. " FROM pg_catalog.pg_class c" |
137
|
|
|
. " LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace" |
138
|
|
|
. " WHERE c.relname ~ '^(" . $tableName . ")$'" |
139
|
|
|
. " AND pg_catalog.pg_table_is_visible(c.oid)" |
140
|
|
|
. " LIMIT 1) AND a.attnum > 0 AND NOT a.attisdropped" |
141
|
|
|
. " ORDER BY a.attnum"; |
142
|
|
|
|
143
|
|
|
$res = $this->connection->query( $sql, __METHOD__ ); |
144
|
|
|
$currentFields = array(); |
145
|
|
|
|
146
|
|
|
foreach ( $res as $row ) { |
147
|
|
|
$type = strtoupper( $row->Type ); |
148
|
|
|
|
149
|
|
|
if ( preg_match( '/^nextval\\(.+\\)/i', $row->Extra ) ) { |
150
|
|
|
$type = 'SERIAL NOT NULL'; |
151
|
|
|
} elseif ( $row->Null != 'YES' ) { |
152
|
|
|
$type .= ' NOT NULL'; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
$currentFields[$row->Field] = $type; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
return $currentFields; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
private function doUpdateField( $tableName, $fieldName, $fieldType, $currentFields, $position, array $tableOptions ) { |
|
|
|
|
162
|
|
|
|
163
|
|
|
$keypos = strpos( $fieldType, ' PRIMARY KEY' ); |
164
|
|
|
|
165
|
|
|
if ( $keypos > 0 ) { |
166
|
|
|
$fieldType = substr( $fieldType, 0, $keypos ); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
$fieldType = strtoupper( $fieldType ); |
170
|
|
|
|
171
|
|
|
if ( !array_key_exists( $fieldName, $currentFields ) ) { |
172
|
|
|
$this->reportMessage( " ... creating field $fieldName ... " ); |
173
|
|
|
$this->connection->query( "ALTER TABLE $tableName ADD \"" . $fieldName . "\" $fieldType", __METHOD__ ); |
174
|
|
|
$this->reportMessage( "done.\n" ); |
175
|
|
|
} elseif ( $currentFields[$fieldName] != $fieldType ) { |
176
|
|
|
$this->reportMessage( " ... changing type of field $fieldName from '$currentFields[$fieldName]' to '$fieldType' ... " ); |
177
|
|
|
|
178
|
|
|
$notnullposnew = strpos( $fieldType, ' NOT NULL' ); |
179
|
|
|
|
180
|
|
|
if ( $notnullposnew > 0 ) { |
181
|
|
|
$fieldType = substr( $fieldType, 0, $notnullposnew ); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
$notnullposold = strpos( $currentFields[$fieldName], ' NOT NULL' ); |
185
|
|
|
$typeold = strtoupper( ( $notnullposold > 0 ) ? substr( $currentFields[$fieldName], 0, $notnullposold ) : $currentFields[$fieldName] ); |
186
|
|
|
|
187
|
|
|
// Added USING statement to avoid |
188
|
|
|
// "Query: ALTER TABLE "smw_object_ids" ALTER COLUMN "smw_proptable_hash" TYPE BYTEA ... |
189
|
|
|
// Error: 42804 ERROR: column "smw_proptable_hash" cannot be cast automatically to type bytea |
190
|
|
|
// HINT: You might need to specify "USING smw_proptable_hash::bytea"." |
191
|
|
|
|
192
|
|
|
if ( $typeold != $fieldType ) { |
193
|
|
|
$sql = "ALTER TABLE " . $tableName . " ALTER COLUMN \"" . $fieldName . "\" TYPE " . $fieldType . " USING \"$fieldName\"::$fieldType"; |
194
|
|
|
$this->connection->query( $sql, __METHOD__ ); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
if ( $notnullposold != $notnullposnew ) { |
198
|
|
|
$sql = "ALTER TABLE " . $tableName . " ALTER COLUMN \"" . $fieldName . "\" " . ( $notnullposnew > 0 ? 'SET' : 'DROP' ) . " NOT NULL"; |
199
|
|
|
$this->connection->query( $sql, __METHOD__ ); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
$this->reportMessage( "done.\n" ); |
203
|
|
|
} else { |
204
|
|
|
$this->reportMessage( " ... field $fieldName is fine.\n" ); |
205
|
|
|
} |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
private function doDropField( $tableName, $fieldName ) { |
209
|
|
|
$this->reportMessage( " ... deleting obsolete field $fieldName ... " ); |
210
|
|
|
$this->connection->query( 'ALTER TABLE ' . $tableName . ' DROP COLUMN "' . $fieldName . '"', __METHOD__ ); |
211
|
|
|
$this->reportMessage( "done.\n" ); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** Index */ |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* @since 2.5 |
218
|
|
|
* |
219
|
|
|
* {@inheritDoc} |
220
|
|
|
*/ |
221
|
|
|
protected function doCreateIndicies( $tableName, array $indexOptions = null ) { |
222
|
|
|
|
223
|
|
|
$indicies = $indexOptions['indicies']; |
224
|
|
|
|
225
|
|
|
// First remove possible obsolete indicies |
226
|
|
|
$this->doDropObsoleteIndicies( $tableName, $indicies ); |
227
|
|
|
|
228
|
|
|
// Add new indexes. |
229
|
|
|
foreach ( $indicies as $indexName => $index ) { |
230
|
|
|
// If the index is an array, it contains the column |
231
|
|
|
// name as first element, and index type as second one. |
232
|
|
|
if ( is_array( $index ) ) { |
233
|
|
|
$columns = $index[0]; |
234
|
|
|
$indexType = count( $index ) > 1 ? $index[1] : 'INDEX'; |
235
|
|
|
} else { |
236
|
|
|
$columns = $index; |
237
|
|
|
$indexType = 'INDEX'; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
$this->doCreateIndex( $tableName, $indexType, $indexName, $columns, $indexOptions ); |
241
|
|
|
} |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
private function doDropObsoleteIndicies( $tableName, array &$indicies ) { |
245
|
|
|
|
246
|
|
|
$tableName = $this->connection->tableName( $tableName, 'raw' ); |
247
|
|
|
$currentIndicies = $this->getIndexInfo( $tableName ); |
248
|
|
|
|
249
|
|
|
foreach ( $currentIndicies as $indexName => $indexColumn ) { |
250
|
|
|
$id = array_search( $indexColumn, $indicies ); |
251
|
|
|
if ( $id !== false || $indexName == 'PRIMARY' ) { |
252
|
|
|
$this->reportMessage( " ... index $indexColumn is fine.\n" ); |
253
|
|
|
|
254
|
|
|
if ( $id !== false ) { |
255
|
|
|
unset( $indicies[$id] ); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
} else { // Duplicate or unrequired index. |
259
|
|
|
$this->doDropIndex( $tableName, $indexName, $indexColumn ); |
260
|
|
|
} |
261
|
|
|
} |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
private function doCreateIndex( $tableName, $indexType, $indexName, $columns, array $indexOptions ) { |
|
|
|
|
265
|
|
|
|
266
|
|
|
if ( $indexType === 'FULLTEXT' ) { |
267
|
|
|
return $this->reportMessage( " ... skipping the fulltext index creation ..." ); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
$tableName = $this->connection->tableName( $tableName, 'raw' ); |
271
|
|
|
$indexName = "{$tableName}_index{$indexName}"; |
272
|
|
|
|
273
|
|
|
$this->reportMessage( " ... creating new index $columns ..." ); |
274
|
|
|
|
275
|
|
|
if ( $this->connection->indexInfo( $tableName, $indexName ) === false ) { |
276
|
|
|
$this->connection->query( "CREATE $indexType $indexName ON $tableName ($columns)", __METHOD__ ); |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
$this->reportMessage( "done.\n" ); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
private function getIndexInfo( $tableName ) { |
283
|
|
|
|
284
|
|
|
$indices = array(); |
285
|
|
|
|
286
|
|
|
$sql = "SELECT i.relname AS indexname," |
287
|
|
|
. " pg_get_indexdef(i.oid) AS indexdef, " |
288
|
|
|
. " replace(substring(pg_get_indexdef(i.oid) from E'\\\\((.*)\\\\)'), ' ' , '') AS indexcolumns" |
289
|
|
|
. " FROM pg_index x" |
290
|
|
|
. " JOIN pg_class c ON c.oid = x.indrelid" |
291
|
|
|
. " JOIN pg_class i ON i.oid = x.indexrelid" |
292
|
|
|
. " LEFT JOIN pg_namespace n ON n.oid = c.relnamespace" |
293
|
|
|
. " LEFT JOIN pg_tablespace t ON t.oid = i.reltablespace" |
294
|
|
|
. " WHERE c.relkind = 'r'::\"char\" AND i.relkind = 'i'::\"char\"" |
295
|
|
|
. " AND c.relname = '" . $tableName . "'" |
296
|
|
|
. " AND NOT pg_get_indexdef(i.oid) ~ '^CREATE UNIQUE INDEX'"; |
297
|
|
|
|
298
|
|
|
$res = $this->connection->query( $sql, __METHOD__ ); |
299
|
|
|
|
300
|
|
|
if ( !$res ) { |
301
|
|
|
return array(); |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
foreach ( $res as $row ) { |
305
|
|
|
$indices[$row->indexname] = $row->indexcolumns; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
return $indices; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
private function doDropIndex( $tableName, $indexName, $columns ) { |
|
|
|
|
312
|
|
|
$this->reportMessage( " ... removing index $columns ..." ); |
313
|
|
|
$this->connection->query( 'DROP INDEX IF EXISTS ' . $indexName, __METHOD__ ); |
314
|
|
|
$this->reportMessage( "done.\n" ); |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** Drop */ |
318
|
|
|
|
319
|
|
|
/** |
320
|
|
|
* @since 2.5 |
321
|
|
|
* |
322
|
|
|
* {@inheritDoc} |
323
|
|
|
*/ |
324
|
|
|
protected function doDropTable( $tableName ) { |
325
|
|
|
$this->connection->query( 'DROP TABLE IF EXISTS ' . $this->connection->tableName( $tableName ), __METHOD__ ); |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
} |
329
|
|
|
|
This check looks from parameters that have been defined for a function or method, but which are not used in the method body.