eZ Publish  [trunk]
ezlintschema.php
Go to the documentation of this file.
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 ?>