/* * Copyright 2007 - 2017 the original author or authors. * * 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 net.sf.jailer.entitygraph.intradatabase; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import net.sf.jailer.ExecutionContext; import net.sf.jailer.configuration.Configuration; import net.sf.jailer.configuration.DBMS; import net.sf.jailer.database.SQLDialect; import net.sf.jailer.database.Session; import net.sf.jailer.database.SqlException; import net.sf.jailer.database.StatementBuilder; import net.sf.jailer.database.UpdateTransformer; import net.sf.jailer.datamodel.Column; import net.sf.jailer.datamodel.DataModel; import net.sf.jailer.datamodel.Filter; import net.sf.jailer.datamodel.PrimaryKey; import net.sf.jailer.datamodel.Table; import net.sf.jailer.entitygraph.EntityGraph; import net.sf.jailer.entitygraph.remote.RemoteEntityGraph; import net.sf.jailer.modelbuilder.JDBCMetaDataBasedModelElementFinder; import net.sf.jailer.progress.ProgressListenerRegistry; import net.sf.jailer.util.JobManager; import net.sf.jailer.util.Quoting; import net.sf.jailer.util.SqlScriptExecutor; /** * Specialized {@link RemoteEntityGraph} for exporting data into a different * schema within the same database. * * @author Ralf Wisser */ public class IntraDatabaseEntityGraph extends RemoteEntityGraph { private boolean upsertOnly; /** * Constructor. * * @param graphID * the unique ID of the graph * @param session * for executing SQL-Statements * @param universalPrimaryKey * the universal primary key * @throws SQLException */ private IntraDatabaseEntityGraph(DataModel dataModel, int graphID, Session session, PrimaryKey universalPrimaryKey, ExecutionContext executionContext) throws SQLException { super(dataModel, graphID, session, universalPrimaryKey, executionContext); upsertOnly = executionContext.getUpsertOnly(); synchronized (this) { upsertStrategy = null; upsertStrategies = new ArrayList<UpsertStrategy>(); upsertStrategies.add(new MergeUS(false)); upsertStrategies.add(new MergeUS(true)); upsertStrategies.add(new UpsertMYSQLUS()); upsertStrategies.add(new UpsertPGUS()); upsertStrategies.add(new UpsertStandardUS()); } quoting = new Quoting(session); } private String defaultSchema = null; private Quoting quoting = null; private Map<Table, Boolean> hasPKPerTable = new HashMap<Table, Boolean>(); private synchronized boolean hasPrimarykey(Session session, Table table) { try { Boolean hasPrimarykey = hasPKPerTable.get(table); if (hasPrimarykey != null) { return hasPrimarykey; } if (defaultSchema == null) { defaultSchema = JDBCMetaDataBasedModelElementFinder.getDefaultSchema(session, session.getSchema()); } String schema = table.getOriginalSchema(""); String mappedSchema = executionContext .getSchemaMapping().get(schema); if (mappedSchema != null) { schema = mappedSchema; } if (schema.length() == 0) { schema = defaultSchema; } schema = quoting.unquote(schema); String tableName = quoting.unquote(table.getUnqualifiedName()); ResultSet resultSet = getPrimaryKeys(session, session.getMetaData(), schema, tableName); hasPrimarykey = resultSet.next(); resultSet.close(); if (!hasPrimarykey) { if (session.getMetaData().storesUpperCaseIdentifiers()) { schema = schema.toUpperCase(); tableName = tableName.toUpperCase(); } else { schema = schema.toLowerCase(); tableName = tableName.toLowerCase(); } resultSet = getPrimaryKeys(session, session.getMetaData(), schema, tableName); hasPrimarykey = resultSet.next(); resultSet.close(); } hasPKPerTable.put(table, hasPrimarykey); return hasPrimarykey; } catch (SQLException e) { return true; } } private ResultSet getPrimaryKeys(Session session, DatabaseMetaData metaData, String schema, String table) throws SQLException { if (DBMS.MySQL.equals(session.dbms)) { return metaData.getPrimaryKeys(schema, null, table); } return metaData.getPrimaryKeys(null, schema, table); } /** * Creates a new entity-graph. * * @param graphID * the unique ID of the graph * @param session * for executing SQL-Statements * @param universalPrimaryKey * the universal primary key * @return the newly created entity-graph */ public static IntraDatabaseEntityGraph create(DataModel dataModel, int graphID, Session session, PrimaryKey universalPrimaryKey, ExecutionContext executionContext) throws SQLException { IntraDatabaseEntityGraph entityGraph = new IntraDatabaseEntityGraph( dataModel, graphID, session, universalPrimaryKey, executionContext); init(graphID, session, executionContext); return entityGraph; } /** * Copies an entity-graph. * * @param newGraphID * the unique ID of the new graph * @param session * for executing SQL-Statements * @return the newly created entity-graph */ public EntityGraph copy(int newGraphID, Session session) throws SQLException { IntraDatabaseEntityGraph entityGraph = create(dataModel, newGraphID, session, universalPrimaryKey, executionContext); entityGraph.setBirthdayOfSubject(birthdayOfSubject); session.executeUpdate("Insert into " + SQLDialect.dmlTableReference(ENTITY, session, executionContext) + "(r_entitygraph, " + universalPrimaryKey.columnList(null) + ", birthday, orig_birthday, type) " + "Select " + newGraphID + ", " + universalPrimaryKey.columnList(null) + ", birthday, birthday, type From " + SQLDialect.dmlTableReference(ENTITY, session, executionContext) + " Where r_entitygraph=" + graphID + ""); return entityGraph; } /** * Finds an entity-graph. * * @param graphID * the unique ID of the graph * @param universalPrimaryKey * the universal primary key * @param session * for executing SQL-Statements * @return the entity-graph */ public EntityGraph find(int graphID, Session session, PrimaryKey universalPrimaryKey) throws SQLException { IntraDatabaseEntityGraph entityGraph = new IntraDatabaseEntityGraph( dataModel, graphID, session, universalPrimaryKey, executionContext); final boolean[] found = new boolean[1]; found[0] = false; session.executeQuery( "Select * From " + SQLDialect.dmlTableReference(ENTITY_GRAPH, session, executionContext) + "Where id=" + graphID + "", new Session.ResultSetReader() { public void readCurrentRow(ResultSet resultSet) throws SQLException { found[0] = true; } public void close() { } }); if (!found[0]) { throw new RuntimeException("entity-graph " + graphID + " not found"); } return entityGraph; } private final String COLUMN_PREFIX = "JALR_"; /** * Reads all entities of a given table which are marked as independent or as * roots. * * @param table * the table * @param orderByPK * not used */ public void readMarkedEntities(Table table, boolean orderByPK) throws SQLException { String selectionSchema = filteredSelectionClause(table, COLUMN_PREFIX, quoting, true); readEntitiesByQuery(table, "Select " + selectionSchema + " From " + SQLDialect.dmlTableReference(ENTITY, session, executionContext) + " E join " + quoting.requote(table.getName()) + " T on " + pkEqualsEntityID(table, "T", "E") + " Where (E.birthday=0 and E.r_entitygraph=" + graphID + " and E.type=" + typeName(table) + ")"); } /** * Reads all entities of a given table. * * @param table * the table * @param orderByPK * not used */ public void readEntities(Table table, boolean orderByPK) throws SQLException { readEntitiesByQuery(table, "Select " + filteredSelectionClause(table, COLUMN_PREFIX, quoting, true) + " From " + SQLDialect.dmlTableReference(ENTITY, session, executionContext) + " E join " + quoting.requote(table.getName()) + " T on " + pkEqualsEntityID(table, "T", "E") + " Where (E.birthday>=0 and E.r_entitygraph=" + graphID + " and E.type=" + typeName(table) + ")"); } /** * Updates columns of a table. * * @param table the table * @param columns the columns; */ public void updateEntities(Table table, Set<Column> columns, OutputStreamWriter scriptFileWriter, DBMS targetConfiguration) throws SQLException { File tmp = Configuration.getInstance().createTempFile(); OutputStreamWriter tmpFileWriter; try { tmpFileWriter = new FileWriter(tmp); UpdateTransformer reader = new UpdateTransformer(table, columns, tmpFileWriter, executionContext.getNumberOfEntities(), getSession(), getSession().dbms, importFilterManager, executionContext); readEntities(table, false, reader); tmpFileWriter.close(); new SqlScriptExecutor(getSession(), executionContext.getNumberOfThreads()).executeScript(tmp.getPath()); } catch (IOException e) { throw new RuntimeException(e); } tmp.delete(); } /** * Reads all entities of a given table. * * @param table * the table * @param sql retrieves the entities */ private void readEntitiesByQuery(Table table, String sql) throws SQLException { boolean tableHasIdentityColumn = false; if (session.dbms.isIdentityInserts()) { for (Column c: table.getColumns()) { if (c.isIdentityColumn) { tableHasIdentityColumn = true; break; } } } long rc; if (tableHasIdentityColumn) { synchronized (session.getConnection()) { session.executeUpdate("SET IDENTITY_INSERT " + qualifiedTableName(table) + " ON"); if (table.getUpsert() || upsertOnly) { rc = upsertRows(table, sql, true); } else { rc = insertRows(table, sql); } session.executeUpdate("SET IDENTITY_INSERT " + qualifiedTableName(table) + " OFF"); } } else { if (table.getUpsert() || upsertOnly) { rc = upsertRows(table, sql, true); } else { rc = insertRows(table, sql); } } ProgressListenerRegistry.getProgressListener().exported(table, rc); } /** * Gets qualified table name. * * @param t * the table * @return qualified name of t */ private String qualifiedTableName(Table t) { String schema = t.getOriginalSchema(""); String mappedSchema = executionContext .getSchemaMapping().get(schema); if (mappedSchema != null) { schema = mappedSchema; } if (schema.length() == 0) { return quoting.requote(t.getUnqualifiedName()); } return quoting.requote(schema) + "." + quoting.requote(t.getUnqualifiedName()); } /** * Checks if columns is part of primary key. * * @param column * the column * @return <code>true</code> if column is part of primary key */ private boolean isPrimaryKeyColumn(Table table, String column) { for (Column c : table.primaryKey.getColumns()) { if (c.name.equalsIgnoreCase(column)) { return true; } } return false; } /** * Inserts rows into a table. Falls back to {@link #upsertRows(Table, String, boolean)} on error. * * @param table the table * @param sqlSelect the rows to insert * @return row count */ private long insertRows(Table table, String sqlSelect) throws SQLException { StringBuilder sb = new StringBuilder(); String labelCSL = insertClause(table, null, null); sb.append("Insert into " + qualifiedTableName(table) + "(" + labelCSL + ") " + sqlSelect); if (!table.primaryKey.getColumns().isEmpty() && !hasPrimarykey(session, table)) { // dont insert if PK constraint is not enforced return upsertRows(table, sqlSelect, true); } boolean silent = session.getSilent(); session.setSilent(true); try { return session.executeUpdate(sb.toString()); } catch (SQLException e) { try { // try upsert return upsertRows(table, sqlSelect, true); } catch (SQLException e2) { // throw original exception throw e; } } finally { session.setSilent(silent); } } /** * Upserts rows of a table. Tries all {@link UpsertStrategy}s until it find one that works. * * @param table the table * @param sqlSelect the rows to be upserted * @param retry if <code>true</code>, try all {@link UpsertStrategy}s * @return row count */ private long upsertRows(Table table, String sqlSelect, boolean retry) throws SQLException { if (table.primaryKey.getColumns().isEmpty()) { throw new RuntimeException("Unable to merge/upsert into table \"" + table.getName() + "\".\n" + "No primary key."); } UpsertStrategy us; boolean done = false; StringBuilder sqlErrorMessages = new StringBuilder(); long rc = 0; synchronized (this) { us = upsertStrategy; } if (us == null) { for (UpsertStrategy strategy: upsertStrategies) { boolean silent = session.getSilent(); session.setSilent(true); try { rc = strategy.upsert(table, sqlSelect); synchronized (this) { upsertStrategy = strategy; us = upsertStrategy; } done = true; break; } catch (SQLException e) { sqlErrorMessages.append("\n" + e.getMessage()); // try another strategy } finally { session.setSilent(silent); } } if (!done) { synchronized (this) { upsertStrategy = upsertStrategies.get(0); us = upsertStrategy; } } } if (!done) { if (retry) { boolean silent = session.getSilent(); session.setSilent(true); try { rc = us.upsert(table, sqlSelect); } catch (SQLException e) { synchronized (this) { upsertStrategy = null; } try { return upsertRows(table, sqlSelect, false); } catch (SQLException e2) { if (e instanceof SqlException) { throw new SqlException(e.getMessage() + sqlErrorMessages, ((SqlException) e).sqlStatement, null); } throw new SQLException(e.getMessage() + sqlErrorMessages, e); } } finally { session.setSilent(silent); } } else { rc = us.upsert(table, sqlSelect); } } return rc; } /** * Gets insert clause for inserting rows of given type with respect of the * column filters. * * @param table * the table to read rows from * @return insert clause */ private String insertClause(Table table, String tableAlias, String columnPrefix) { StringBuilder sb = new StringBuilder(); boolean first = true; for (Column c : table.getColumns()) { if (c.isVirtualOrBlocked(session)) { continue; } if (!first) { sb.append(", "); } if (tableAlias != null) { sb.append(tableAlias + "."); } sb.append(prefixColumnName(columnPrefix, quoting, c)); first = false; } return sb.toString(); } private long insertWhereNotExists(Table table, String sqlSelect) throws SQLException { StatementBuilder upsertInsertStatementBuilder = new StatementBuilder(1); String insertHead = "Insert into " + qualifiedTableName(table) + "(" + insertClause(table, null, null) + ") "; StringBuffer whereForTerminator = new StringBuffer(""); // assemble 'where' boolean f = true; for (Column pk : table.primaryKey.getColumns()) { if (!f) { whereForTerminator.append(" and "); } f = false; whereForTerminator.append("T." + quoting.requote(pk.name) + "=Q." + prefixColumnName(COLUMN_PREFIX, quoting, pk)); } insertHead += "Select " + insertClause(table, "Q", COLUMN_PREFIX) + " From ("; StringBuffer terminator = new StringBuffer(") as Q" + " Where not exists (Select * from " + qualifiedTableName(table) + " T " + "Where "); terminator.append(whereForTerminator + ")"); upsertInsertStatementBuilder.append(insertHead, sqlSelect, "", terminator.toString()); String sql = upsertInsertStatementBuilder.build(); return session.executeUpdate(sql); } /** * A strategy to upsert (merge) rows. */ private interface UpsertStrategy { long upsert(Table table, String sqlSelect) throws SQLException; } /** * Uses "MERGE INTO" statements to upsert rows. */ private class MergeUS implements UpsertStrategy { private final boolean withSemicolon; public MergeUS(boolean withSemicolon) { this.withSemicolon = withSemicolon; } @Override public long upsert(Table table, String sqlSelect) throws SQLException { String labelCSL = insertClause(table, null, null); StatementBuilder upsertInsertStatementBuilder = new StatementBuilder(1); String insertHead = "Insert into " + qualifiedTableName(table) + "(" + labelCSL + ") "; StringBuffer whereForTerminator = new StringBuffer(""); // assemble 'where' boolean f = true; for (Column pk : table.primaryKey.getColumns()) { if (!f) { whereForTerminator.append(" and "); } f = false; whereForTerminator.append(applyImportFilter("T." + quoting.requote(pk.name), pk) + "=Q." + prefixColumnName(COLUMN_PREFIX, quoting, pk)); } insertHead = "MERGE INTO " + qualifiedTableName(table) + " T USING("; StringBuffer terminator = new StringBuffer(") Q ON(" + whereForTerminator + ") "); StringBuffer sets = new StringBuffer(); StringBuffer tSchema = new StringBuffer(); StringBuffer iSchema = new StringBuffer(); for (Column column : table.getColumns()) { if (!isPrimaryKeyColumn(table, column.name)) { if (sets.length() > 0) { sets.append(", "); } sets.append("T." + quoting.requote(column.name) + "=Q." + prefixColumnName(COLUMN_PREFIX, quoting, column)); } if (tSchema.length() > 0) { tSchema.append(", "); } tSchema.append(quoting.requote(column.name)); if (iSchema.length() > 0) { iSchema.append(", "); } iSchema.append("Q." + prefixColumnName(COLUMN_PREFIX, quoting, column)); } if (sets.length() > 0) { terminator.append("WHEN MATCHED THEN UPDATE SET " + sets + " "); } terminator.append("WHEN NOT MATCHED THEN INSERT (" + tSchema + ") VALUES(" + iSchema + ")"); upsertInsertStatementBuilder.append(insertHead, sqlSelect, "", terminator.toString()); String sql = upsertInsertStatementBuilder.build(); return session.executeUpdate(sql + (withSemicolon? ";" : "")); } }; /** * Uses "INSERT" statements followed by "UPDATE" statements. (Postgres dialect); */ private class UpsertPGUS implements UpsertStrategy { @Override public long upsert(Table table, String sqlSelect) throws SQLException { StringBuffer sets = new StringBuffer(); StringBuffer where = new StringBuffer(); for (Column column : table.getColumns()) { if (!isPrimaryKeyColumn(table, column.name)) { if (sets.length() > 0) { sets.append(", "); } sets.append(quoting.requote(column.name) + "=Q." + prefixColumnName(COLUMN_PREFIX, quoting, column)); } else { if (where.length() > 0) { where.append(" and "); } where.append("S." + quoting.requote(column.name) + "=" + applyImportFilter("T." + quoting.requote(column.name), column)); } } if (sets.length() == 0) { // nothing to do return 0; } String sql = "Update " + qualifiedTableName(table) + " S set " + sets + " from (" + sqlSelect + " and (" + where + ")) Q "; long rc = session.executeUpdate(sql); return rc + insertWhereNotExists(table, sqlSelect); } }; /** * Uses "INSERT" statements followed by "UPDATE" statements. (MySQL dialect); */ private class UpsertMYSQLUS implements UpsertStrategy { @Override public long upsert(Table table, String sqlSelect) throws SQLException { StringBuffer sets = new StringBuffer(); StringBuffer where = new StringBuffer(); for (Column column : table.getColumns()) { if (!isPrimaryKeyColumn(table, column.name)) { if (sets.length() > 0) { sets.append(", "); } sets.append("S." + quoting.requote(column.name) + "=Q." + prefixColumnName(COLUMN_PREFIX, quoting, column)); } else { if (where.length() > 0) { where.append(" and "); } where.append(applyImportFilter("S." + quoting.requote(column.name), column) + "=Q." + prefixColumnName(COLUMN_PREFIX, quoting, column)); } } if (sets.length() == 0) { // nothing to do return 0; } String sql = "Update (" + sqlSelect + ") Q join " + qualifiedTableName(table) + " S on " + where + " set " + sets; long rc = session.executeUpdate(sql); return rc + insertWhereNotExists(table, sqlSelect); } }; /** * Uses "INSERT" statements followed by "UPDATE" statements. (Standard SQL); */ private class UpsertStandardUS implements UpsertStrategy { @Override public long upsert(Table table, String sqlSelect) throws SQLException { StringBuffer nonPKList = new StringBuffer(); StringBuffer nonPKListQ = new StringBuffer(); StringBuffer where = new StringBuffer(); StringBuffer whereT = new StringBuffer(); for (Column column : table.getColumns()) { if (!isPrimaryKeyColumn(table, column.name)) { if (nonPKList.length() > 0) { nonPKList.append(", "); } nonPKList.append(quoting.requote(column.name)); if (nonPKListQ.length() > 0) { nonPKListQ.append(", "); } nonPKListQ.append("Q." + prefixColumnName(COLUMN_PREFIX, quoting, column)); } else { if (where.length() > 0) { where.append(" and "); } where.append(applyImportFilter("S." + quoting.requote(column.name), column) + "=Q." + prefixColumnName(COLUMN_PREFIX, quoting, column)); if (whereT.length() > 0) { whereT.append(" and "); } whereT.append(applyImportFilter("S." + quoting.requote(column.name), column) + "=T." + quoting.requote(column.name)); } } if (nonPKList.length() == 0) { // nothing to do return 0; } String sql = "Update " + qualifiedTableName(table) + " S set (" + nonPKList + ")" + " = (Select " + nonPKListQ + " From (" + sqlSelect + ") Q Where " + where + ") " + "Where exists (" + sqlSelect + " and (" + whereT + "))"; long rc = session.executeUpdate(sql); return rc + insertWhereNotExists(table, sqlSelect); } }; private UpsertStrategy upsertStrategy; private List<UpsertStrategy> upsertStrategies; /** * Insert the values of columns with non-derived-import-filters into the local database. */ @Override public void fillAndWriteMappingTables(JobManager jobManager, final OutputStreamWriter receiptWriter, int numberOfEntities, final Session targetSession, final DBMS targetDBMSConfiguration, DBMS dbmsConfiguration) throws IOException, SQLException { if (importFilterManager != null) { File tmp = Configuration.getInstance().createTempFile(); OutputStreamWriter tmpFileWriter; tmpFileWriter = new FileWriter(tmp); importFilterManager.createMappingTables(dbmsConfiguration, tmpFileWriter); tmpFileWriter.close(); new SqlScriptExecutor(getSession(), executionContext.getNumberOfThreads()).executeScript(tmp.getPath()); tmp.delete(); tmp = Configuration.getInstance().createTempFile(); tmpFileWriter = new FileWriter(tmp); tmpFileWriter.write("-- sync\n"); importFilterManager.fillAndWriteMappingTables(this, jobManager, tmpFileWriter, numberOfEntities, targetSession, targetDBMSConfiguration); tmpFileWriter.close(); new SqlScriptExecutor(getSession(), executionContext.getNumberOfThreads()).executeScript(tmp.getPath()); tmp.delete(); } } private String applyImportFilter(String oldValue, Column column) { Filter filter = column.getFilter(); if (filter != null && importFilterManager != null) { if (!filter.isApplyAtExport()) { return importFilterManager.transform(column, oldValue); } } return oldValue; } /** * Creates the DROP-statements for the mapping tables. */ @Override public void dropMappingTables(OutputStreamWriter result, DBMS targetDBMSConfiguration) throws IOException, SQLException { if (importFilterManager != null) { File tmp = Configuration.getInstance().createTempFile(); OutputStreamWriter tmpFileWriter; tmpFileWriter = new FileWriter(tmp); importFilterManager.dropMappingTables(tmpFileWriter); tmpFileWriter.close(); new SqlScriptExecutor(getSession(), executionContext.getNumberOfThreads()).executeScript(tmp.getPath()); tmp.delete(); } } }