diff --git a/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DatabaseMerger.java b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DatabaseMerger.java new file mode 100644 index 0000000000000000000000000000000000000000..76dc61fc6c3bda0653d5eb878318fdac51af79f0 --- /dev/null +++ b/dbmigration/source/java/ch/systemsx/cisd/dbmigration/DatabaseMerger.java @@ -0,0 +1,439 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.systemsx.cisd.dbmigration; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import javax.sql.DataSource; + +import org.apache.commons.dbcp.BasicDataSource; + +import ch.systemsx.cisd.common.exceptions.CheckedExceptionTunnel; + +/** + * Generic merger of two databases + * + * @author Franz-Josef Elmer + */ +public class DatabaseMerger +{ + private static final class TableConnection + { + private final TableDefinition dependentTable; + + private final TableColumnDefinition dependentColumn; + + public TableConnection(TableDefinition dependentTable, TableColumnDefinition dependentColumn) + { + this.dependentTable = dependentTable; + this.dependentColumn = dependentColumn; + } + + public final TableDefinition getDependentTable() + { + return dependentTable; + } + + public final TableColumnDefinition getDependentColumn() + { + return dependentColumn; + } + + @Override + public String toString() + { + return dependentTable.getTableName() + "." + dependentColumn.getColumnName(); + } + + } + + private static final class TableColumnDefinition implements Iterable<TableConnection> + { + private boolean primaryKey; + + private long largestPrimaryKey = -1; + + private boolean foreignKey; + + private String columnName; + + private String dataTypeName; + + private List<TableConnection> connections = new ArrayList<TableConnection>(); + + public void addConnection(TableDefinition fkTableDefinition, + TableColumnDefinition fkColumnDefinition) + { + connections.add(new TableConnection(fkTableDefinition, fkColumnDefinition)); + } + + public Iterator<TableConnection> iterator() + { + return connections.iterator(); + } + + public final long getLargestPrimaryKey() + { + return largestPrimaryKey; + } + + public final void setLargestPrimaryKey(long nextFreePrimaryKey) + { + this.largestPrimaryKey = nextFreePrimaryKey; + } + + public final boolean isForeignKey() + { + return foreignKey; + } + + public final void setForeignKey(boolean foreignKey) + { + this.foreignKey = foreignKey; + } + + public final boolean isPrimaryKey() + { + return primaryKey; + } + + public final void setPrimaryKey(boolean primaryKey) + { + this.primaryKey = primaryKey; + } + + public final String getColumnName() + { + return columnName; + } + + public final void setColumnName(String columnName) + { + this.columnName = columnName; + } + + public final String getDataTypeName() + { + return dataTypeName; + } + + public final void setDataTypeName(String dataType) + { + this.dataTypeName = dataType; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj instanceof TableColumnDefinition == false) + { + return false; + } + TableColumnDefinition columnDefinition = (TableColumnDefinition) obj; + return columnDefinition.columnName.equals(columnName); + } + + @Override + public int hashCode() + { + return columnName.hashCode(); + } + + @Override + public String toString() + { + return (primaryKey ? "*" : " ") + columnName + " " + dataTypeName + + (foreignKey ? " (fk) " : " ") + + (largestPrimaryKey < 0 ? "" : Long.toString(largestPrimaryKey) + " ") + + (connections.isEmpty() ? "" : connections.toString()); + } + } + + private static final class TableDefinition implements Iterable<TableColumnDefinition>, + Comparable<TableDefinition> + { + private final String tableName; + + private final Map<String, TableColumnDefinition> columns = + new TreeMap<String, TableColumnDefinition>(); + + TableDefinition(String tableName) + { + this.tableName = tableName; + } + + public final String getTableName() + { + return tableName; + } + + public void add(TableColumnDefinition columnDefinition) + { + columns.put(columnDefinition.getColumnName(), columnDefinition); + } + + public void defineColumnAsPrimaryKey(String columnName) + { + getColumnDefinition(columnName).setPrimaryKey(true); + } + + public TableColumnDefinition getColumnDefinition(String columnName) + { + TableColumnDefinition columnDefinition = columns.get(columnName); + if (columnDefinition == null) + { + throw new IllegalArgumentException("No column '" + columnName + + "' defined^in table '" + tableName + "'."); + } + return columnDefinition; + } + + public Iterator<TableColumnDefinition> iterator() + { + return columns.values().iterator(); + } + + public int compareTo(TableDefinition tableDefinition) + { + return tableName.compareTo(tableDefinition.getTableName()); + } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(); + builder.append(tableName).append(":"); + for (TableColumnDefinition columnDefinition : columns.values()) + { + builder.append("\n ").append(columnDefinition); + } + return builder.toString(); + } + + } + + private static final class DatabaseDefinition implements Iterable<TableDefinition> + { + private final Map<String, TableDefinition> tableDefinitions = + new TreeMap<String, TableDefinition>(); + + public void add(TableDefinition tableDefinition) + { + tableDefinitions.put(tableDefinition.getTableName(), tableDefinition); + } + + public TableDefinition getTableDefinition(String tableName) + { + TableDefinition tableDefinition = tableDefinitions.get(tableName); + if (tableDefinition == null) + { + throw new IllegalArgumentException("Unknown table '" + tableName + "'."); + } + return tableDefinition; + } + + public void connect(String pkTableName, String pkColumnName, String fkTableName, + String fkColumnName) + { + TableDefinition pkTableDefinition = getTableDefinition(pkTableName); + TableColumnDefinition pkColumnDefinition = + pkTableDefinition.getColumnDefinition(pkColumnName); + TableDefinition fkTableDefinition = getTableDefinition(fkTableName); + TableColumnDefinition fkColumnDefinition = + fkTableDefinition.getColumnDefinition(fkColumnName); + fkColumnDefinition.setForeignKey(true); + pkColumnDefinition.addConnection(fkTableDefinition, fkColumnDefinition); + } + + public Iterator<TableDefinition> iterator() + { + return tableDefinitions.values().iterator(); + } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(); + for (TableDefinition tableDefinition : tableDefinitions.values()) + { + builder.append(tableDefinition).append('\n'); + } + return builder.toString(); + } + + } + + private DatabaseDefinition databaseDefinition; + + /** + * Creates an instance for the specified data source. Gathers all necessary metadata. + */ + public DatabaseMerger(DataSource dataSource) + { + databaseDefinition = new DatabaseDefinition(); + try + { + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + DatabaseMetaData metaData = connection.getMetaData(); + ResultSet rs = metaData.getTables(null, null, null, new String[] + { "TABLE" }); + while (rs.next()) + { + databaseDefinition.add(new TableDefinition(rs.getString("TABLE_NAME"))); + } + for (TableDefinition tableDefinition : databaseDefinition) + { + String tableName = tableDefinition.getTableName(); + rs = metaData.getColumns(null, "%", tableName, "%"); + while (rs.next()) + { + TableColumnDefinition columnDefinition = new TableColumnDefinition(); + columnDefinition.setColumnName(rs.getString("COLUMN_NAME")); + columnDefinition.setDataTypeName(rs.getString("TYPE_NAME")); + tableDefinition.add(columnDefinition); + } + } + for (TableDefinition tableDefinition : databaseDefinition) + { + String tableName = tableDefinition.getTableName(); + rs = metaData.getPrimaryKeys(null, null, tableName); + while (rs.next()) + { + tableDefinition.defineColumnAsPrimaryKey(rs.getString("COLUMN_NAME")); + } + rs = metaData.getExportedKeys(null, null, tableName); + while (rs.next()) + { + String pkTableName = rs.getString("PKTABLE_NAME"); + String pkColumnName = rs.getString("PKCOLUMN_NAME"); + String fkTableName = rs.getString("FKTABLE_NAME"); + String fkColumnName = rs.getString("FKCOLUMN_NAME"); + databaseDefinition + .connect(pkTableName, pkColumnName, fkTableName, fkColumnName); + } + } + for (TableDefinition tableDefinition : databaseDefinition) + { + for (TableColumnDefinition columnDefinition : tableDefinition) + { + if (columnDefinition.isPrimaryKey()) + { + String tableName = tableDefinition.getTableName(); + String columnName = columnDefinition.getColumnName(); + String sql = "select max(" + columnName + ") from " + tableName; + rs = statement.executeQuery(sql); + rs.next(); + columnDefinition.setLargestPrimaryKey(rs.getLong(1)); + } + } + } + statement.close(); + connection.close(); + } catch (SQLException ex) + { + throw CheckedExceptionTunnel.wrapIfNecessary(ex); + } + } + + /** + * Returns database definition necessary for merging. + */ + public DatabaseDefinition getDatabaseDefinition() + { + return databaseDefinition; + } + + /** + * Returns the definitions of all tables depending directly or indirectly on the specified + * tables. + */ + public Set<TableDefinition> getTablesDependingOn(String... tableNames) + { + Set<TableDefinition> collection = new TreeSet<TableDefinition>(); + Set<String> visitedTableDefinitions = new HashSet<String>(); + for (String tableName : tableNames) + { + collectTablesDependingOn(databaseDefinition.getTableDefinition(tableName), collection, + visitedTableDefinitions); + } + + return collection; + } + + private void collectTablesDependingOn(TableDefinition tableDefinition, + Set<TableDefinition> collection, Set<String> visitedTableDefinitions) + { + if (visitedTableDefinitions.contains(tableDefinition.getTableName())) + { + return; + } + visitedTableDefinitions.add(tableDefinition.getTableName()); + for (TableColumnDefinition tableColumnDefinition : tableDefinition) + { + for (TableConnection tableConnection : tableColumnDefinition) + { + TableDefinition dependentTable = tableConnection.getDependentTable(); + collection.add(dependentTable); + collectTablesDependingOn(dependentTable, collection, visitedTableDefinitions); + } + } + } + + public static void main(String[] args) + { + String database = "lims_dev"; + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("org.postgresql.Driver"); + dataSource.setUrl("jdbc:postgresql://localhost/" + database); + dataSource.setUsername("postgres"); + dataSource.setPassword(""); + dataSource.setMinIdle(0); + dataSource.setMaxIdle(0); + + DatabaseMerger merger = new DatabaseMerger(dataSource); + DatabaseDefinition databaseDefinition = merger.getDatabaseDefinition(); + System.out.println(databaseDefinition); + + System.out.print("Tables which do not depend on table 'database_instances': "); + Set<TableDefinition> tables = merger.getTablesDependingOn("database_instances"); + for (TableDefinition tableDefinition : databaseDefinition) + { + if (tables.contains(tableDefinition) == false) + { + System.out.print(tableDefinition.getTableName() + " "); + } + } + System.out.println(); + } +}