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