Completed
Push — master ( cc37b1...6647f2 )
by mw
38:04
created

PostgresTableBuilder::getStandardFieldType()   C

Complexity

Conditions 13
Paths 13

Size

Total Lines 30
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 26
c 1
b 0
f 0
nc 13
nop 1
dl 0
loc 30
rs 5.1234

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 ) {
0 ignored issues
show
Unused Code introduced by
The parameter $position 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...
Unused Code introduced by
The parameter $tableOptions 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...
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 ) {
0 ignored issues
show
Unused Code introduced by
The parameter $indexOptions 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...
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 ) {
0 ignored issues
show
Unused Code introduced by
The parameter $tableName 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...
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