DBMigrationAction.java

/*
 * Copyright 2013 Gregory Graham.
 *
 * 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 nz.co.gregs.dbvolution.actions;

import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.ArrayList;
import nz.co.gregs.dbvolution.databases.DBDatabase;
import nz.co.gregs.dbvolution.DBMigration;
import nz.co.gregs.dbvolution.DBQuery;
import nz.co.gregs.dbvolution.DBRow;
import nz.co.gregs.dbvolution.databases.DBStatement;
import nz.co.gregs.dbvolution.databases.QueryIntention;
import nz.co.gregs.dbvolution.databases.definitions.DBDefinition;
import nz.co.gregs.dbvolution.datatypes.DBLargeObject;
import nz.co.gregs.dbvolution.datatypes.QueryableDatatype;
import nz.co.gregs.dbvolution.exceptions.UnableToAccessDBMigrationFieldException;
import nz.co.gregs.dbvolution.expressions.DBExpression;
import nz.co.gregs.dbvolution.internal.properties.PropertyWrapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Provides support for the abstract concept of migrating rows using one or more
 * tables to another table.
 *
 * @author Gregory Graham
 * @param <R> the resulting DBRow using this DBMigrationAction
 */
public class DBMigrationAction<R extends DBRow> extends DBAction {

	private static final long serialVersionUID = 1l;

	private static final Log LOG = LogFactory.getLog(DBQueryInsertAction.class);

	private final DBMigration<R> sourceMigration;
	private final DBRow[] extraExamples;

	/**
	 * Creates a DBMigrate action for the row.
	 *
	 * @param migration the mapping to transform the source data
	 * @param resultRow the resulting DBRow produced by the mapping
	 * @param examples extra examples used to reduce the source data set.
	 */
	public DBMigrationAction(DBMigration<R> migration, DBRow resultRow, DBRow... examples) {
		super(resultRow, QueryIntention.MIGRATION);
		sourceMigration = migration;
		extraExamples = examples;
	}

	/**
	 * Perform the migration
	 *
	 * @param database the database used by this action
	 * @return a DBActionList of the migration's effects
	 * @throws SQLException SQL Exceptions may be thrown
	 */
	public DBActionList migrate(DBDatabase database) throws SQLException {
		DBMigrationAction<R> migrate = new DBMigrationAction<>(sourceMigration, getRow());
		final DBActionList executedActions = database.executeDBAction(migrate);
		return executedActions;
	}

	@Override
	@SuppressWarnings("unchecked")
	public ArrayList<String> getSQLStatements(DBDatabase db) {
		DBRow table = getRow();
		DBDefinition defn = db.getDefinition();
		String allColumns = processAllFieldsForMigration(db, (R) getRow());

		ArrayList<String> strs = new ArrayList<>();
		strs.add(defn.beginInsertLine()
				+ defn.formatTableName(table)
				+ defn.beginInsertColumnList()
				+ allColumns
				+ defn.endInsertColumnList()
				+ getSQLForExtractionQuery(db));
		return strs;
	}

	public String getSQLForExtractionQuery(DBDatabase database) {
		DBQuery query = database.getDBQuery();
		query.setBlankQueryAllowed(sourceMigration.getBlankQueryAllowed());
		query.setCartesianJoinsAllowed(sourceMigration.getCartesianJoinsAllowed());
		
		addTablesAndExpressions(query);
		query.addExtraExamples(extraExamples);
		query.setSortOrder(sourceMigration.getSortColumns());
		return query.getSQLForQuery();
	}
	
	public void addTablesAndExpressions(DBQuery query) {
		final DBRow mapper = sourceMigration.getMapper();
		var optionalTables = sourceMigration.getOptionalTables();
		Field[] fields = mapper.getClass().getFields();
		if (fields.length == 0) {
			throw new UnableToAccessDBMigrationFieldException(this, null);
		}
		for (Field field : fields) {
			field.setAccessible(true);
			final Object value;
			try {
				value = field.get(mapper);
				if (value != null && DBRow.class.isAssignableFrom(value.getClass())) {
					if (value instanceof DBRow) {
						final DBRow dbRow = (DBRow) value;
						dbRow.removeAllFieldsFromResults();
						if (optionalTables.contains(dbRow)) {
							query.addOptional(dbRow);
						} else {
							query.add(dbRow);
						}
					}
				} else if (value != null && QueryableDatatype.class.isAssignableFrom(value.getClass())) {
					final QueryableDatatype<?> qdtValue = (QueryableDatatype) value;
					if ((value instanceof QueryableDatatype) && qdtValue.hasColumnExpression()) {
						query.addExpressionColumn(value, qdtValue);
						final DBExpression[] columnExpressions = qdtValue.getColumnExpression();
						for (DBExpression columnExpression : columnExpressions) {
							if (!columnExpression.isAggregator()) {
								query.addGroupByColumn(value, columnExpression);
							}
						}
					}
				}
			} catch (IllegalArgumentException | IllegalAccessException ex) {
				throw new UnableToAccessDBMigrationFieldException(this, field, ex);
			}
		}
	}

	@Override
	public DBActionList execute(DBDatabase db) throws SQLException {
		DBActionList actions = new DBActionList(new DBMigrationAction<>(sourceMigration, getRow(), extraExamples));

		try (DBStatement statement = db.getDBStatement()) {
			for (String sql : getSQLStatements(db)) {
				try {
					statement.execute("MIGRATION INSERT", QueryIntention.BULK_INSERT, sql);
				} catch (SQLException sqlex) {
					try {
						statement.execute("MIGRATION INSERT", QueryIntention.BULK_INSERT, sql);
					} catch (SQLException ex) {
						throw new SQLException(ex.getLocalizedMessage() + ":" + sql, ex);
					}
				}
			}
		}
		return actions;
	}

	private String processAllFieldsForMigration(DBDatabase database, R row) {
		StringBuilder allColumns = new StringBuilder();
		StringBuilder allValues = new StringBuilder();
		StringBuilder allChangedColumns = new StringBuilder();
		StringBuilder allSetValues = new StringBuilder();
		DBDefinition defn = database.getDefinition();
		var props = row.getColumnPropertyWrappers();
		String allColumnSeparator = "";
		String columnSeparator = "";
		String valuesSeparator = defn.beginValueClause();
		String allValuesSeparator = defn.beginValueClause();
		for (var prop : props) {
			// BLOBS are not inserted normally so don't include them
			if (prop.isColumn()) {
				final QueryableDatatype<?> qdt = prop.getQueryableDatatype();
				if (qdt != null) {
					if (!(qdt instanceof DBLargeObject)) {
						//support for inserting empty rows in a table with an autoincrementing pk
						if (!prop.isAutoIncrement()) {
							allColumns
									.append(allColumnSeparator)
									.append(" ")
									.append(defn.formatColumnName(prop.columnName()));
							allColumnSeparator = defn.getValuesClauseColumnSeparator();
							// add the value
							allValues
									.append(allValuesSeparator)
									.append(qdt.toSQLString(database.getDefinition()));
							allValuesSeparator = defn.getValuesClauseValueSeparator();
						}
						if (qdt.hasBeenSet()) {
							// nice normal columns
							// Add the column
							allChangedColumns
									.append(columnSeparator)
									.append(" ")
									.append(defn.formatColumnName(prop.columnName()));
							columnSeparator = defn.getValuesClauseColumnSeparator();
							// add the value
							allSetValues
									.append(valuesSeparator)
									.append(qdt.toSQLString(database.getDefinition()));
							valuesSeparator = defn.getValuesClauseValueSeparator();
						}
					}
				}
			}
		}
		allValues.append(defn.endValueClause());
		allSetValues.append(defn.endValueClause());
		return allColumns.toString();
	}

	@Override
	protected DBActionList getRevertDBActionList() {
		throw new UnsupportedOperationException("Reverting A Migration Is Not Possible Yet.");
	}
}