|
eZ Publish
[trunk]
|
00001 <?php 00002 /** 00003 * File containing the eZLintSchema class. 00004 * 00005 * @copyright Copyright (C) 1999-2012 eZ Systems AS. All rights reserved. 00006 * @license http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2 00007 * @version //autogentag// 00008 * @package lib 00009 */ 00010 00011 /*! 00012 \class eZLintSchema ezlintschema.php 00013 \ingroup eZDbSchema 00014 \brief Provides lint checking of database schemas 00015 00016 Checks a given schema by going trough all tables, fields and indexes 00017 and corrects any mistakes. The result is a new schema which is returned 00018 in schema(). The new schema can then be used to diff against the original 00019 and output the changes. 00020 00021 The current rules apply: 00022 - Table names must not exceed 26 characters, configurable in dbschema.ini (LintChecker/TableLimit) 00023 - Field names must not exceed 30 characters, configurable in dbschema.ini (LintChecker/FieldLimit) 00024 - Index names must not exceed 30 characters, configurable in dbschema.ini (LintChecker/IndexLimit) 00025 - Index names must not be the same as table names 00026 - String fields cannot have NOT NULL and an empty string as DEFAULT value. 00027 - Primary keys must be named PRIMARY 00028 00029 The lint checker works by taking in another DB Schema object as parameter 00030 to the constructor. All calls will be forwarded to this object so it will 00031 work as though it were a real schema. 00032 The exception are the schema(), data() and validate() methods which makes 00033 sure the schema is correct. 00034 00035 To check if the schema has been checked yet call isLintChecked(). 00036 To fetch the DB schema which is checked use otherSchema(). 00037 00038 */ 00039 00040 class eZLintSchema extends eZDBSchemaInterface 00041 { 00042 /*! 00043 Initializes the lint checker with a foreign db schema. 00044 00045 \param $db A dummy parameter, pass \c false. 00046 \param $otherSchema The db schema that should be checked 00047 */ 00048 function eZLintSchema( $db, $otherSchema ) 00049 { 00050 $this->eZDBSchemaInterface( $db ); 00051 $this->OtherSchema = $otherSchema; 00052 $this->CorrectSchema = false; 00053 $this->IsLintChecked = false; 00054 } 00055 00056 /*! 00057 Runs the lint checker on the database schema in otherSchema() 00058 and returns the new schema that is correct. 00059 */ 00060 function schema( $params = array() ) 00061 { 00062 if ( $this->IsLintChecked ) 00063 { 00064 return $this->CorrectSchema; 00065 } 00066 00067 $params = array_merge( array( 'meta_data' => false, 00068 'format' => 'generic' ), 00069 $params ); 00070 00071 $this->CorrectSchema = $this->OtherSchema->schema( $params ); 00072 $this->lintCheckSchema( $this->CorrectSchema ); 00073 $this->IsLintChecked = true; 00074 return $this->CorrectSchema; 00075 } 00076 00077 /*! 00078 Runs lint checker on all tables, indexes and fields. 00079 */ 00080 function validate() 00081 { 00082 return $this->lintCheckSchema( $this->CorrectSchema ); 00083 } 00084 00085 /*! 00086 \return The schema object which is being lint checked. 00087 */ 00088 function otherSchema() 00089 { 00090 return $this->OtherSchema; 00091 } 00092 00093 /*! 00094 \return \c true if the lint checker has been run on the schema. 00095 */ 00096 function isLintChecked() 00097 { 00098 return $this->IsLintChecked; 00099 } 00100 00101 /*! 00102 \return A modified version of \a $identifier that is guaranteed to be shorter than \a $limit 00103 */ 00104 function shortenIdentifier( $identifier, $limit, $shortenList ) 00105 { 00106 reset( $shortenList ); 00107 // Replace one word at a time until we have a string that is short 00108 // enough, or we run out of replace words 00109 while ( strlen( $identifier ) > $limit and 00110 current( $shortenList ) !== false ) 00111 { 00112 $from = key( $shortenList ); 00113 $to = current( $shortenList ); 00114 next( $shortenList ); 00115 $identifier = str_replace( $from, $to, $identifier ); 00116 } 00117 00118 // It is still to large so we just cut it off 00119 if ( strlen( $identifier ) > $limit ) 00120 { 00121 $identifier = substr( $identifier, 0, $limit ); 00122 } 00123 return $identifier; 00124 } 00125 00126 /*! 00127 \private 00128 Goes trough all tables, fields and indexes and makes sure they have valid names. 00129 \return \c false if something was fixed, \c true otherwise. 00130 */ 00131 function lintCheckSchema( &$schema ) 00132 { 00133 $status = true; 00134 00135 $ini = eZINI::instance( 'dbschema.ini' ); 00136 00137 // A mapping table that maps from a long name to a short name 00138 // This will be used if an identifier/name is too long 00139 $shortenList = $ini->variable( 'LintChecker', 'NameMap' ); 00140 00141 // Limitation on the length of identifiers/names 00142 // Oracle is the database with the most limit (30 characters) so the 00143 // limit values must be equal or lower to that. 00144 $tableNameLimit = (int)$ini->variable( 'LintChecker', 'TableLimit' ); 00145 $fieldNameLimit = (int)$ini->variable( 'LintChecker', 'FieldLimit' ); 00146 $indexNameLimit = (int)$ini->variable( 'LintChecker', 'IndexLimit' ); 00147 00148 // Tables which do not get lint checked, they are currently 00149 // handled with workarounds in the various schema handlers 00150 // Note: The fields and indexes are still checked. 00151 $ignoredTableList = $ini->variable( 'LintChecker', 'IgnoredTables' ); 00152 00153 // Fields which do not get lint checked, they are currently 00154 // handled with workarounds in the various schema handlers 00155 $list = $ini->variable( 'LintChecker', 'IgnoredFields' ); 00156 $ignoredFieldList = array(); 00157 foreach ( $list as $entry ) 00158 { 00159 list( $tableName, $fieldName ) = explode( '.', $entry, 2 ); 00160 if ( !isset( $ignoredFieldList[$tableName] ) ) 00161 $ignoredFieldList[$tableName] = array(); 00162 $ignoredFieldList[$tableName][] = $fieldName; 00163 } 00164 00165 // Fields which do not get lint checked, they are currently 00166 // handled with workarounds in the various schema handlers 00167 $list = $ini->variable( 'LintChecker', 'IgnoredFieldSyntax' ); 00168 $ignoredFieldSyntaxList = array(); 00169 foreach ( $list as $entry ) 00170 { 00171 list( $tableName, $fieldName ) = explode( '.', $entry, 2 ); 00172 if ( !isset( $ignoredFieldList[$tableName] ) ) 00173 $ignoredFieldSyntaxList[$tableName] = array(); 00174 $ignoredFieldSyntaxList[$tableName][] = $fieldName; 00175 } 00176 00177 // Indexes which do not get lint checked, they are currently 00178 // handled with workarounds in the various schema handlers 00179 $ignoredIndexList = $ini->variable( 'LintChecker', 'IgnoredIndexes' ); 00180 00181 $badTables = array(); 00182 foreach ( $schema as $tableName => $tableDef ) 00183 { 00184 // Skip the info structure, this is not a table 00185 if ( $tableName == '_info' ) 00186 continue; 00187 00188 $existingTableName = $tableName; 00189 $tableComments = array(); 00190 00191 // If table is not in ignore list we check the name 00192 if ( !in_array( $tableName, $ignoredTableList ) ) 00193 { 00194 // identifiers must be 30 or less 00195 // for tables we require 26 or less to allow adding suffix or prefix for indexes etc. 00196 if ( strlen( $tableName ) > $tableNameLimit ) 00197 { 00198 $tableComment = "Table names must not exceed $tableNameLimit characters,\n'$tableName' is " . strlen( $tableName ) . " characters,\ndatabases like Oracle will have problems with this."; 00199 $tableName = $this->shortenIdentifier( $tableName, $tableNameLimit, $shortenList ); 00200 $tableComment .= "\nNew name is '$tableName'"; 00201 $tableComments[] = $tableComment; 00202 $status = false; 00203 } 00204 00205 if ( strcmp( $tableName, $existingTableName ) != 0 ) 00206 { 00207 $badTables[] = array( 'from' => $existingTableName, 00208 'to' => $tableName ); 00209 } 00210 } 00211 00212 if ( isset( $tableDef['fields'] ) ) 00213 { 00214 $badFields = array(); 00215 foreach ( $tableDef['fields'] as $fieldName => $fieldDef ) 00216 { 00217 $comments = array(); 00218 $existingFieldName = $fieldName; 00219 00220 // Do we ignore the field name? 00221 if ( !isset( $ignoredFieldList[$existingTableName] ) or 00222 !in_array( $fieldName, $ignoredFieldList[$existingTableName] ) ) 00223 { 00224 00225 // identifiers must be 30 or less 00226 if ( strlen( $fieldName ) > $fieldNameLimit ) 00227 { 00228 $comment = "Field names must not exceed $fieldNameLimit characters,\n'$fieldName' in table '$existingTableName' is " . strlen( $fieldName ) . " characters,\ndatabases like Oracle will have problems with this."; 00229 $fieldName = $this->shortenIdentifier( $fieldName, $fieldNameLimit, $shortenList ); 00230 $comment .= "\nNew name is '$fieldName'"; 00231 $comments[] = $comment; 00232 $status = false; 00233 } 00234 } 00235 00236 if ( !isset( $ignoredFieldSyntaxList[$existingTableName] ) or 00237 !in_array( $fieldName, $ignoredFieldSyntaxList[$existingTableName] ) ) 00238 { 00239 /* Temporarily disabled 00240 if ( in_array( $fieldDef['type'], 00241 array( 'varchar', 'char', 00242 'longtext', 'mediumtext', 'shorttext' ) ) and 00243 isset( $fieldDef['not_null'] ) and 00244 $fieldDef['not_null'] and 00245 $fieldDef['default'] === '' ) 00246 { 00247 $comments[] = "The string type " . $fieldDef['type'] . " ($existingTableName.$fieldName) cannot have NOT NULL defined and an empty string as DEFAULT value\nDatabase like Oracle will have problems with this."; 00248 $status = false; 00249 } 00250 */ 00251 } 00252 00253 if ( strcmp( $existingFieldName, $fieldName ) != 0 ) 00254 { 00255 $badFields[] = array( 'from' => $existingFieldName, 00256 'to' => $fieldName ); 00257 } 00258 00259 if ( count( $comments ) > 0 ) 00260 { 00261 $schema[$existingTableName]['fields'][$existingFieldName]['comments'] = $comments; 00262 foreach ( $comments as $comment ) 00263 { 00264 eZDebug::writeWarning( $comment, __METHOD__ ); 00265 } 00266 } 00267 } 00268 00269 foreach ( $badFields as $badField ) 00270 { 00271 $schema[$existingTableName]['fields'][$badField['to']] = $schema[$existingTableName]['fields'][$badField['from']]; 00272 // unset( $schema[$existingTableName]['fields'][$badField['from']] ); 00273 $schema[$existingTableName]['fields'][$badField['from']]['removed'] = true; 00274 } 00275 } 00276 00277 if ( isset( $tableDef['indexes'] ) ) 00278 { 00279 $badIndexes = array(); 00280 foreach ( $tableDef['indexes'] as $indexName => $indexDef ) 00281 { 00282 // Primary key 00283 if ( $indexDef['type'] == 'primary' ) 00284 continue; 00285 00286 // Do we ignore the index? 00287 if ( in_array( $indexName, $ignoredIndexList ) ) 00288 continue; 00289 00290 $comments = array(); 00291 00292 $existingIndexName = $indexName; 00293 if ( isset( $schema[$indexName] ) ) 00294 { 00295 $comment = "Index named '$indexName' has same name as an existing table,\ndatabases like PostgreSQL and Oracle will have problems with this."; 00296 $indexFieldText = ''; 00297 $i = 0; 00298 foreach ( $indexDef['fields'] as $fieldDef ) 00299 { 00300 if ( $i > 0 ) 00301 $indexFieldText .= '_'; 00302 if ( is_array( $fieldDef ) ) 00303 { 00304 $indexFieldText .= $fieldDef['name']; 00305 } 00306 else 00307 { 00308 $indexFieldText .= $fieldDef; 00309 } 00310 } 00311 $indexName = $indexName . '_' . $indexFieldText . '_i'; 00312 $comment .= "\nNew name is '$indexName'"; 00313 $comments[] = $comment; 00314 $status = false; 00315 } 00316 00317 // Primary indexes must be named PRIMARY 00318 if ( $indexDef['type'] == 'primary' and 00319 $indexName != 'PRIMARY' ) 00320 { 00321 $comment = "Index named '$indexName' which is a primary key must be named PRIMARY."; 00322 $indexName = "PRIMARY"; 00323 $comments[] = $comment; 00324 $status = false; 00325 } 00326 00327 // identifiers must be 30 or less 00328 if ( strlen( $indexName ) > $indexNameLimit ) 00329 { 00330 $comment = "Index names must not exceed $indexNameLimit characters,\n'$indexName' is " . strlen( $indexName ) . " characters,\ndatabases like Oracle will have problems with this."; 00331 $indexName = $this->shortenIdentifier( $indexName, $indexNameLimit, $shortenList ); 00332 $comment .= "\nNew name is '$indexName'"; 00333 $comments[] = $comment; 00334 $status = false; 00335 } 00336 00337 // Check if there are some database specific entries 00338 foreach ( $indexDef['fields'] as $fieldDef ) 00339 { 00340 if ( is_array( $fieldDef ) ) 00341 { 00342 $fieldName = $fieldDef['name']; 00343 foreach ( $fieldDef as $fdName => $fdValue ) 00344 { 00345 if ( preg_match( "#^([a-z0-9]+):#", $fdName, $matches ) ) 00346 { 00347 $dbName = $matches[1]; 00348 $comments[] = "Found database specific entry ($dbName) at index $existingIndexName.$fieldName"; 00349 $status = false; 00350 } 00351 } 00352 } 00353 } 00354 00355 if ( strcmp( $existingIndexName, $indexName ) != 0 ) 00356 { 00357 $badIndexes[] = array( 'from' => $existingIndexName, 00358 'to' => $indexName ); 00359 } 00360 if ( count( $comments ) > 0 ) 00361 { 00362 $schema[$existingTableName]['indexes'][$existingIndexName]['comments'] = $comments; 00363 foreach ( $comments as $comment ) 00364 { 00365 eZDebug::writeWarning( $comment, __METHOD__ ); 00366 } 00367 } 00368 } 00369 00370 foreach ( $badIndexes as $badIndex ) 00371 { 00372 $schema[$existingTableName]['indexes'][$badIndex['to']] = $schema[$existingTableName]['indexes'][$badIndex['from']]; 00373 $schema[$existingTableName]['indexes'][$badIndex['from']]['removed'] = true; 00374 } 00375 } 00376 00377 if ( count( $tableComments ) > 0 ) 00378 { 00379 $schema[$existingTableName]['comments'] = $tableComments; 00380 foreach ( $tableComments as $comment ) 00381 { 00382 eZDebug::writeWarning( $comment, __METHOD__ ); 00383 } 00384 } 00385 } 00386 foreach ( $badTables as $badTable ) 00387 { 00388 $schema[$badTable['to']] = $schema[$badTable['from']]; 00389 $schema[$badTable['from']]['removed'] = true; 00390 } 00391 return $status; 00392 } 00393 00394 /*! 00395 Forwards request to data() on the otherSchema() object. 00396 */ 00397 function data( $schema = false, $tableNameList = false, $params = array() ) 00398 { 00399 return $this->OtherSchema->data( $schema, $tableNameList, $params ); 00400 } 00401 00402 /*! 00403 Forwards request to generateSchemaFile() on the otherSchema() object. 00404 */ 00405 function generateSchemaFile( $schema, $params = array() ) 00406 { 00407 return $this->OtherSchema->generateSchemaFile( $schema, $params ); 00408 } 00409 00410 /*! 00411 Forwards request to generateUpgradeFile() on the otherSchema() object. 00412 */ 00413 function generateUpgradeFile( $differences, $params = array() ) 00414 { 00415 return $this->OtherSchema->generateUpgradeFile( $differences, $params ); 00416 } 00417 00418 /*! 00419 Forwards request to generateDataFile() on the otherSchema() object. 00420 */ 00421 function generateDataFile( $schema, $data, $params ) 00422 { 00423 return $this->OtherSchema->generateDataFile( $schema, $data, $params ); 00424 } 00425 00426 /*! 00427 Forwards request to generateTableSchema() on the otherSchema() object. 00428 */ 00429 function generateTableSchema( $table, $tableDef, $params ) 00430 { 00431 return $this->OtherSchema->generateTableSchema( $table, $tableDef, $params ); 00432 } 00433 00434 /*! 00435 Forwards request to generateTableInsert() on the otherSchema() object. 00436 */ 00437 function generateTableInsert( $tableName, $tableDef, $dataEntries, $params ) 00438 { 00439 return $this->OtherSchema->generateTableInsert( $tableName, $tableDef, $dataEntries, $params ); 00440 } 00441 00442 /*! 00443 Forwards request to generateDropTable() on the otherSchema() object. 00444 */ 00445 function generateDropTable( $table, $params ) 00446 { 00447 return $this->OtherSchema->generateDropTable( $table, $params ); 00448 } 00449 00450 /*! 00451 Forwards request to generateAddFieldSql() on the otherSchema() object. 00452 */ 00453 function generateAddFieldSql( $table, $field_name, $added_field, $params ) 00454 { 00455 return $this->OtherSchema->generateAddFieldSql( $table, $field_name, $added_field, $params ); 00456 } 00457 00458 /*! 00459 Forwards request to generateAlterFieldSql() on the otherSchema() object. 00460 */ 00461 function generateAlterFieldSql( $table, $field_name, $changed_field, $params ) 00462 { 00463 return $this->OtherSchema->generateAlterFieldSql( $table, $field_name, $changed_field, $params ); 00464 } 00465 00466 /*! 00467 Forwards request to generateDropFieldSql() on the otherSchema() object. 00468 */ 00469 function generateDropFieldSql( $table, $field_name, $params ) 00470 { 00471 return $this->OtherSchema->generateDropFieldSql( $table, $field_name, $params ); 00472 } 00473 00474 /*! 00475 Forwards request to generateAddIndexSql() on the otherSchema() object. 00476 */ 00477 function generateAddIndexSql( $table, $index_name, $added_index, $params ) 00478 { 00479 return $this->OtherSchema->generateAddIndexSql( $table, $index_name, $added_index, $params ); 00480 } 00481 00482 /*! 00483 Forwards request to generateDropIndexSql() on the otherSchema() object. 00484 */ 00485 function generateDropIndexSql( $table, $index_name, $removed_index, $params ) 00486 { 00487 return $this->OtherSchema->generateDropIndexSql( $table, $index_name, $removed_index, $params ); 00488 } 00489 00490 /*! 00491 Forwards request to isMultiInsertSupported() on the otherSchema() object. 00492 */ 00493 function isMultiInsertSupported() 00494 { 00495 return $this->OtherSchema->isMultiInsertSupported(); 00496 } 00497 00498 /*! 00499 Forwards request to generateDataValueTextSQL() on the otherSchema() object. 00500 */ 00501 function generateDataValueTextSQL( $fieldDef, $value ) 00502 { 00503 return $this->OtherSchema->generateDataValueTextSQL( $fieldDef, $value ); 00504 } 00505 00506 /*! 00507 Forwards request to schemaType() on the otherSchema() object. 00508 */ 00509 function schemaType() 00510 { 00511 return $this->OtherSchema->schemaType(); 00512 } 00513 00514 /*! 00515 Forwards request to schemaName() on the otherSchema() object. 00516 */ 00517 function schemaName() 00518 { 00519 return $this->OtherSchema->schemaName(); 00520 } 00521 00522 /// \privatesection 00523 /// eZDBSchemaInterface object which should be lint checked 00524 public $OtherSchema; 00525 /// The corrected schema 00526 public $CorrectSchema; 00527 /// Whether the schema has been checked or not 00528 public $IsLintChecked; 00529 } 00530 00531 ?>