package fr.lteconsulting.hexa.server.qpath; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import fr.lteconsulting.hexa.server.qpath.DatabaseDescription.FieldDescription; import fr.lteconsulting.hexa.server.qpath.DatabaseDescription.FieldReference; import fr.lteconsulting.hexa.server.qpath.DatabaseDescription.TableDescription; import fr.lteconsulting.hexa.server.tools.Trace; public class DatabaseDescriptionInspector { DatabaseMySQLDialect dialect = new DatabaseMySQLDialect(); // returns an array containing the description of the database schema public DatabaseDescription getDatabaseDescription( Database db, DatabaseHelper dbh ) { DatabaseDescription dbDesc = new DatabaseDescription( db.getCurrentDatabase() ); ArrayList<String> tables = dbh.getTables(); for( String table : tables ) { TableDescription tableDesc = dbDesc.addTable( table ); DBResults fields = db.sql( "DESCRIBE " + table ); int fieldNameColumn = fields.getColumnIndex( "Field" ); int typeColumn = fields.getColumnIndex( "Type" ); int canNullColumn = fields.getColumnIndex( "Null" ); int defaultColumn = fields.getColumnIndex( "Default" ); int extraColumn = fields.getColumnIndex( "Extra" ); int keyColumn = fields.getColumnIndex( "Key" ); while( fields.next() ) { String fieldName = fields.getString( fieldNameColumn ); if( fieldName.startsWith( "synchro_server" ) ) continue; String type = fields.getString( typeColumn ); String canNull = fields.getString( canNullColumn ); String defaultValue = fields.getString( defaultColumn ); String extra = fields.getString( extraColumn ); String key = fields.getString( keyColumn ); FieldDescription fieldDesc = tableDesc.addField( fieldName, type, canNull, defaultValue, extra, key ); DBResults info = db.sql( "SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='" + dbDesc.name + "' AND TABLE_NAME='" + table + "' AND COLUMN_NAME='" + fieldName + "'" ); if( info.getRowCount() != 1 ) { System.out.println( "BIG PROBLEM, column has no or multiple definitions !!!" ); return null; } int commentColumn = info.getColumnIndex( "COLUMN_COMMENT" ); if( commentColumn > 0 ) { info.next(); // seek to the first and only result row fieldDesc.comment = info.getString( commentColumn ); } } // Inspect unicity constraints String sql = "select CONSTRAINT_NAME, COLUMN_NAME from information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA='" + dbDesc.name + "' AND TABLE_NAME='" + table + "' AND REFERENCED_TABLE_NAME IS NULL AND CONSTRAINT_NAME != 'PRIMARY' ORDER BY CONSTRAINT_NAME, ORDINAL_POSITION"; DBResults constraintsRecords = db.sql( sql ); int constraintNameColumn = constraintsRecords.getColumnIndex( "CONSTRAINT_NAME" ); int columnNameColumn = constraintsRecords.getColumnIndex( "COLUMN_NAME" ); String currentConstraintName = null; List<String> currentConstraintColumnNames = null; while( constraintsRecords.next() ) { String recordConstraintName = constraintsRecords.getString( constraintNameColumn ); if( currentConstraintName == null || !currentConstraintName.equalsIgnoreCase( recordConstraintName ) ) { // initialize a new constraint description currentConstraintName = recordConstraintName; currentConstraintColumnNames = tableDesc.addUnicityConstraint( recordConstraintName ); } // register that referenced column into the constraint currentConstraintColumnNames.add( constraintsRecords.getString( columnNameColumn ) ); } } // Show constraints DBResults constraints = db.sql( "SELECT * FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA='" + dbDesc.name + "'" ); int tableColumn = constraints.getColumnIndex( "TABLE_NAME" ); int columnNameColumn = constraints.getColumnIndex( "COLUMN_NAME" ); int constraintNameColumn = constraints.getColumnIndex( "CONSTRAINT_NAME" ); int referencedTableColumn = constraints.getColumnIndex( "REFERENCED_TABLE_NAME" ); int referencedFieldColumn = constraints.getColumnIndex( "REFERENCED_COLUMN_NAME" ); while( constraints.next() ) { String table = constraints.getString( tableColumn ); String fieldName = constraints.getString( columnNameColumn ); String constraintName = constraints.getString( constraintNameColumn ); if( constraintName.equalsIgnoreCase( "PRIMARY" ) ) { dbDesc.tables.get( table ).fields.get( fieldName ).primaryKey = true; } else { String refTable = constraints.getString( referencedTableColumn ); String refField = constraints.getString( referencedFieldColumn ); if( refTable == null || refField == null ) continue; // What is that ? dbDesc.tables.get( table ).fields.get( fieldName ).addReferenceField( refTable, refField, constraintName ); } } return dbDesc; } class SetComparison { HashSet<String> newItems = new HashSet<String>(); HashSet<String> removedItems = new HashSet<String>(); HashSet<String> maybeModifiedItems = new HashSet<String>(); public SetComparison() { } public void compareSets( Set<String> current, Set<String> target ) { removedItems.addAll( current ); for( String tgt : target ) { if( current.contains( tgt ) ) { maybeModifiedItems.add( tgt ); removedItems.remove( tgt ); } else { newItems.add( tgt ); } } } } // returns the array of sql statements to be executed to update the db // currentDB is a kind of a hack : for the deletion of a constraint in the // target database, we need to query the db for all the constraint names to // delete // them public ArrayList<String> getSqlForUpdateDb( DatabaseDescription currentDbDesc, DatabaseDescription targetDbDesc, boolean fDoDelete, boolean fTableNameUpperCase ) { Trace.push(); Trace.it( "Database comparison" ); ArrayList<String> sqls = new ArrayList<String>(); ArrayList<String> sqlRefs = new ArrayList<String>(); ArrayList<String> sqlConstraints = new ArrayList<String>(); ArrayList<String> newTables = new ArrayList<String>(); ArrayList<String> maybeModifiedTables = new ArrayList<String>(); HashSet<String> removedTables = new HashSet<String>(); for( TableDescription t : currentDbDesc.tables.values() ) removedTables.add( t.name ); for( TableDescription table : targetDbDesc.tables.values() ) { TableDescription existingTable = currentDbDesc.tables.get( table.name ); if( existingTable == null ) { newTables.add( table.name ); } else { maybeModifiedTables.add( table.name ); removedTables.remove( table.name ); } } // new tables for( String newTableName : newTables ) { TableDescription desc = targetDbDesc.tables.get( newTableName ); Trace.it( "New table " + newTableName ); String sql = dialect.getSqlForCreateTable( fTableNameUpperCase, newTableName, desc.fields.values() ); sqls.add( sql ); for( FieldDescription fieldDesc : desc.fields.values() ) checkReferences( fTableNameUpperCase, newTableName, null, fieldDesc, sqlRefs ); } // removed tables for( String removedTableName : removedTables ) { if( fDoDelete ) { Trace.it( "Removed table " + removedTableName ); String sql = dialect.getSqlForDropTable( fTableNameUpperCase, removedTableName ); sqls.add( sql ); } else { Trace.it( "IGNORED - Removed table " + removedTableName ); } } // maybe modified tables for( String maybeModifiedTable : maybeModifiedTables ) { TableDescription tgt = targetDbDesc.tables.get( maybeModifiedTable ); TableDescription cur = currentDbDesc.tables.get( maybeModifiedTable ); assert tgt.name.equalsIgnoreCase( cur.name ); SetComparison fieldsComparison = new SetComparison(); fieldsComparison.compareSets( cur.fields.keySet(), tgt.fields.keySet() ); Trace.it( "Comparing tables " + maybeModifiedTable ); // new fields for( String newFieldName : fieldsComparison.newItems ) { Trace.it( "New field " + newFieldName ); FieldDescription newField = tgt.fields.get( newFieldName ); String sql = dialect.getSqlForAddColumn( fTableNameUpperCase, maybeModifiedTable, newField ); sqls.add( sql ); } // removed fields for( String removedFieldName : fieldsComparison.removedItems ) { if( fDoDelete ) { Trace.it( "Removed field " + removedFieldName ); String sql = dialect.getSqlForDropColumn( fTableNameUpperCase, maybeModifiedTable, removedFieldName ); sqls.add( sql ); } else { Trace.it( "IGNORED - Removed field " + removedFieldName ); } } // Maybe modified fields for( String fieldName : fieldsComparison.maybeModifiedItems ) { FieldDescription curField = cur.fields.get( fieldName ); FieldDescription tgtField = tgt.fields.get( fieldName ); // modified field String targetColumnSql = dialect.getColumnSql( tgtField ); String curColumnSql = dialect.getColumnSql( curField ); if( !targetColumnSql.equalsIgnoreCase( curColumnSql ) ) { Trace.it( "Modified field " + fieldName ); String sql = dialect.getSqlForChangeColumn( fTableNameUpperCase, maybeModifiedTable, fieldName, tgtField ); sqls.add( sql ); } // modified references checkReferences( fTableNameUpperCase, maybeModifiedTable, curField, tgtField, sqlRefs ); } // unicity constraints SetComparison constraintsComparison = new SetComparison(); constraintsComparison.compareSets( cur.unicityConstraints.keySet(), tgt.unicityConstraints.keySet() ); // new constraints for( String constraintName : constraintsComparison.newItems ) { List<String> constraintFields = tgt.unicityConstraints.get( constraintName ); String sql = dialect.getSqlForCreateConstraint( fTableNameUpperCase, tgt.name, constraintName, constraintFields ); if( sql == null ) continue; sqlConstraints.add( sql ); } // removed constraints for( String constraintName : constraintsComparison.removedItems ) { String sql = dialect.getSqlForDropIndex( fTableNameUpperCase, tgt.name, constraintName ); sqlConstraints.add( sql ); } // maybe modified constraints for( String constraintName : constraintsComparison.maybeModifiedItems ) { // Test if constraints are equals if( areConstraintsIdentical( cur.unicityConstraints.get( constraintName ), tgt.unicityConstraints.get( constraintName ) ) ) continue; // Delete ... String sql = dialect.getSqlForDropIndex( fTableNameUpperCase, tgt.name.toUpperCase(), constraintName ); sqlConstraints.add( sql ); // ... and recreate List<String> constraintFields = tgt.unicityConstraints.get( constraintName ); sql = dialect.getSqlForCreateConstraint( fTableNameUpperCase, tgt.name, constraintName, constraintFields ); sqlConstraints.add( sql ); } } // merge sqls and sqlRefs and sqlConstraints sqls.addAll( sqlRefs ); sqls.addAll( sqlConstraints ); Trace.pop(); return sqls; } private boolean areConstraintsIdentical( List<String> set1, List<String> set2 ) { if( set1 == null && set2 == null ) return true; if( set1 == null || set2 == null ) return false; for( String entry1 : set1 ) if( !set2.contains( entry1 ) ) return false; for( String entry2 : set2 ) if( !set1.contains( entry2 ) ) return false; return true; } private void checkReferences( boolean fTableNameUpperCase, String tableName, FieldDescription curField, FieldDescription tgtField, ArrayList<String> sqls ) { ArrayList<FieldReference> curRefs; if( curField != null ) curRefs = curField.fieldReferences; else curRefs = new ArrayList<FieldReference>(); ArrayList<FieldReference> tgtRefs = tgtField.fieldReferences; for( FieldReference ref : curRefs ) { // if a similar ref is not found in $tgtRefs, that is this reference // must be deleted if( !tgtField.hasReference( ref.table, ref.field ) ) { for( String constraintName : ref.constraintNames ) { String sql = dialect.getSqlForDropForeignKey( fTableNameUpperCase, tableName, constraintName ); sqls.add( sql ); } } } for( FieldReference ref : tgtRefs ) { // if a similar ref is not found in $curRefs, that is this reference // is a new to be created if( curField == null || !curField.hasReference( ref.table, ref.field ) ) { String sql = dialect.getSqlForCreateForeignKey( fTableNameUpperCase, tableName, tgtField.name, ref.table, ref.field ); sqls.add( sql ); } } } }