/* * 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.database; import java.io.IOException; import java.io.OutputStreamWriter; import java.sql.Date; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.sql.Types; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import net.sf.jailer.ExecutionContext; import net.sf.jailer.configuration.DBMS; import net.sf.jailer.database.Session.AbstractResultSetReader; import net.sf.jailer.database.Session.ResultSetReader; import net.sf.jailer.datamodel.Column; import net.sf.jailer.datamodel.Filter; import net.sf.jailer.datamodel.Table; import net.sf.jailer.util.CellContentConverter; import net.sf.jailer.util.Quoting; import net.sf.jailer.util.SqlUtil; /** * A {@link ResultSetReader} that writes the read rows as UPDATE statements * into the export-script. * * @author Ralf Wisser */ public class UpdateTransformer extends AbstractResultSetReader { /** * The table to read from. */ private final Table table; /** * The columns to update. */ private final Set<Column> columns; /** * The file to write to. */ private final OutputStreamWriter scriptFileWriter; /** * Number of columns. */ private int columnCount; /** * Labels of columns. */ private String[] columnLabel = null; /** * Labels of columns as comma separated list. */ private String labelCSL; /** * For building compact insert-parts of upsert-statements. */ private Map<String, StatementBuilder> upsertInsertStatementBuilder = new HashMap<String, StatementBuilder>(); /** * Maximum length of SQL values list (for generated inserts). */ private final int maxBodySize; /** * For quoting of column names. */ private final Quoting quoting; /** * Current session; */ private final Session session; /** * Configuration of the target DBMS. */ private final DBMS targetDBMSConfiguration; /** * SQL Dialect. */ private final SQLDialect currentDialect; /** * Transforms {@link Filter} into SQL-expressions. */ private final ImportFilterTransformer importFilterTransformer; /** * The execution context. */ private final ExecutionContext executionContext; /** * Constructor. * * @param table the table to read from * @param scriptFileWriter the file to write to * @param maxBodySize maximum length of SQL values list (for generated inserts) * @param session the session * @param targetDBMSConfiguration configuration of the target DBMS */ public UpdateTransformer(Table table, Set<Column> columns, OutputStreamWriter scriptFileWriter, int maxBodySize, Session session, DBMS targetDBMSConfiguration, ImportFilterTransformer importFilterTransformer, ExecutionContext executionContext) throws SQLException { this.executionContext = executionContext; this.targetDBMSConfiguration = targetDBMSConfiguration; this.maxBodySize = maxBodySize; this.table = table; this.columns = columns; this.scriptFileWriter = scriptFileWriter; this.currentDialect = targetDBMSConfiguration.getSqlDialect(); this.quoting = new Quoting(session); this.importFilterTransformer = importFilterTransformer; if (targetDBMSConfiguration != null && targetDBMSConfiguration != session.dbms) { if (targetDBMSConfiguration.getIdentifierQuoteString() != null) { this.quoting.setIdentifierQuoteString(targetDBMSConfiguration.getIdentifierQuoteString()); } } this.session = session; selectionClause = table.getSelectionClause(session); } private final List<Column> selectionClause; /** * Converts cell content to SQL literals. * * @param cellContentConverter converter * @param resultSet points to current row * @param i current result set index * @param content cell content * @return SQL literal */ protected String convertToSql(CellContentConverter cellContentConverter, ResultSet resultSet, int i, Object content) throws SQLException { String cVal = cellContentConverter.toSql(content); Column column = selectionClause.get(i - 1); Filter filter = column.getFilter(); if (filter != null && importFilterTransformer != null) { if (!filter.isApplyAtExport()) { return importFilterTransformer.transform(column, cVal); } } if (content != null && filter != null && filter.getExpression().trim().startsWith(Filter.LITERAL_PREFIX)) { return content.toString(); } return cVal; } private Set<String> columnNamesLower = new HashSet<String>(); /** * Reads result-set and writes into export-script. */ public void readCurrentRow(ResultSet resultSet) throws SQLException { if (columnLabel == null) { columnCount = getMetaData(resultSet).getColumnCount(); columnLabel = new String[columnCount + 1]; labelCSL = ""; for (int i = 1; i <= columnCount; ++i) { String mdColumnLabel = quoting.quote(getMetaData(resultSet).getColumnLabel(i)); columnLabel[i] = mdColumnLabel; if (labelCSL.length() > 0) { labelCSL += ", "; } labelCSL += columnLabel[i]; } for (Column column: columns) { columnNamesLower.add(column.name.toLowerCase()); } } try { StringBuffer valueList = new StringBuffer(""); StringBuffer namedValues = new StringBuffer(""); boolean f = true; CellContentConverter cellContentConverter = getCellContentConverter(resultSet, session, targetDBMSConfiguration); for (int i = 1; i <= columnCount; ++i) { Object content = null; if (columnLabel[i] == null) { continue; } if (!isPrimaryKeyColumn(columnLabel[i]) && !columnNamesLower.contains(columnLabel[i].toLowerCase())) { continue; } if (content == null) { content = cellContentConverter.getObject(resultSet, i); if (resultSet.wasNull()) { content = null; } } if (!f) { namedValues.append(", "); valueList.append(", "); } f = false; String cVal = convertToSql(cellContentConverter, resultSet, i, content); valueList.append(cVal); namedValues.append(cVal + " " + columnLabel[i]); } if (table.primaryKey.getColumns().isEmpty()) { throw new RuntimeException("Unable to merge/upsert into table \"" + table.getName() + "\".\n" + "No primary key."); } Map<String, String> val = new HashMap<String, String>(); StringBuffer valuesWONull = new StringBuffer(""); StringBuffer columnsWONull = new StringBuffer(""); f = true; for (int i = 1; i <= columnCount; ++i) { if (columnLabel[i] == null) { continue; } Object content = cellContentConverter.getObject(resultSet, i); if (resultSet.wasNull()) { content = null; } String cVal = convertToSql(cellContentConverter, resultSet, i, content); if (DBMS.POSTGRESQL.equals(targetDBMSConfiguration) && (content instanceof Date || content instanceof Timestamp)) { // explicit cast needed cVal = "timestamp " + cVal; } if (DBMS.POSTGRESQL.equals(targetDBMSConfiguration)) { // explicit cast needed int mdColumnType = getMetaData(resultSet).getColumnType(i); if (mdColumnType == Types.TIME) { cVal = "time " + cVal; } } val.put(columnLabel[i], cVal); if (content != null) { if (!f) { valuesWONull.append(", "); columnsWONull.append(", "); } f = false; valuesWONull.append(cVal); columnsWONull.append(columnLabel[i]); } } f = true; StringBuffer whereForTerminator = new StringBuffer(""); StringBuffer where = new StringBuffer(""); StringBuffer whereWOAlias = new StringBuffer(""); // assemble 'where' for sub-select and update for (Column pk: table.primaryKey.getColumns()) { if (!f) { whereForTerminator.append(" and "); where.append(" and "); whereWOAlias.append(" and "); } f = false; whereForTerminator.append("T." + quoting.requote(pk.name) + "=Q." + quoting.requote(pk.name)); String value; String name = quoting.quote(pk.name); if (val.containsKey(name)) { value = val.get(name); } else if (val.containsKey(name.toLowerCase())) { value = val.get(name.toLowerCase()); } else { value = val.get(name.toUpperCase()); } where.append("T." + quoting.requote(pk.name) + "=" + value); whereWOAlias.append(quoting.requote(pk.name) + "=" + value); } if (currentDialect.getUpsertMode() == UPSERT_MODE.MERGE) { // MERGE INTO JL_TMP T USING (SELECT 1 c1, 2 c2 from dual) incoming // ON (T.c1 = incoming.c1) // WHEN MATCHED THEN UPDATE SET T.c2 = incoming.c2 String 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 (int i = 1; i <= columnCount; ++i) { if (columnLabel[i] == null) { continue; } if (!isPrimaryKeyColumn(columnLabel[i])) { if (columnNamesLower.contains(columnLabel[i].toLowerCase())) { if (sets.length() > 0) { sets.append(", "); } sets.append("T." + columnLabel[i] + "=Q." + columnLabel[i]); } } if (tSchema.length() > 0) { tSchema.append(", "); } tSchema.append("T." + columnLabel[i]); if (iSchema.length() > 0) { iSchema.append(", "); } iSchema.append("Q." + columnLabel[i]); } if (sets.length() > 0) { terminator.append("WHEN MATCHED THEN UPDATE SET " + sets + ";\n"); } StatementBuilder sb = upsertInsertStatementBuilder.get(insertHead); if (sb == null) { sb = new StatementBuilder(maxBodySize); upsertInsertStatementBuilder.put(insertHead, sb); } String item = "Select " + valueList + " from dual"; if (!sb.isAppendable(insertHead, item)) { writeToScriptFile(sb.build(), true); } if (sb.isEmpty()) { item = "Select " + namedValues + " from dual"; } sb.append(insertHead, item, " UNION ALL ", terminator.toString()); } if (currentDialect.getUpsertMode() != UPSERT_MODE.MERGE) { StringBuffer insert = new StringBuffer(""); insert.append("Update " + qualifiedTableName(table) + " set "); f = true; for (int i = 1; i <= columnCount; ++i) { if (!columnNamesLower.contains(columnLabel[i].toLowerCase())) { continue; } if (isPrimaryKeyColumn(columnLabel[i])) { continue; } if (!f) { insert.append(", "); } f = false; insert.append(columnLabel[i] + "=" + val.get(columnLabel[i])); } if (!f) { insert.append(" Where " + whereWOAlias + ";\n"); writeToScriptFile(insert.toString(), true); } } } catch (IOException e) { throw new RuntimeException(e); } } /** * 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(String column) { for (Column c: table.primaryKey.getColumns()) { if (c.name.equalsIgnoreCase(column)) { return true; } } return false; } /** * Flushes the export-reader. */ public void flush() { try { for (StatementBuilder sb: upsertInsertStatementBuilder.values()) { writeToScriptFile(sb.build(), true); } } catch (IOException e) { throw new RuntimeException(e); } } /** * Flushes the export-reader. */ public void close() { flush(); } /** * Writes into script. */ private void writeToScriptFile(String content, boolean wrap) throws IOException { synchronized (scriptFileWriter) { if (wrap && DBMS.ORACLE.equals(targetDBMSConfiguration)) { scriptFileWriter.write(SqlUtil.splitDMLStatement(content, 2400)); } else { scriptFileWriter.write(content); } } } }