All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cn.lead2success.ddlutils.platform.PlatformImplBase Maven / Gradle / Ivy

There is a newer version: 1.0.0_21071202
Show newest version
package cn.lead2success.ddlutils.platform;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

import cn.lead2success.ddlutils.DatabaseOperationException;
import cn.lead2success.ddlutils.DdlUtilsException;
import cn.lead2success.ddlutils.Platform;
import cn.lead2success.ddlutils.PlatformInfo;
import cn.lead2success.ddlutils.alteration.*;
import cn.lead2success.ddlutils.dynabean.SqlDynaClass;
import cn.lead2success.ddlutils.dynabean.SqlDynaProperty;
import cn.lead2success.ddlutils.model.*;
import cn.lead2success.ddlutils.util.JdbcSupport;
import cn.lead2success.ddlutils.util.SqlTokenizer;
import org.apache.commons.beanutils.DynaBean;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.sql.*;
import java.util.*;

/**
 * Base class for platform implementations.
 * 
 * @version $Revision: 231110 $
 */
public abstract class PlatformImplBase extends JdbcSupport implements Platform {
	/** The default name for models read from the database, if no name as given. */
	protected static final String MODEL_DEFAULT_NAME = "default";

	/** The log for this platform. */
	private final Log _log = LogFactory.getLog(getClass());

	/** The platform info. */
	private PlatformInfo _info = new PlatformInfo();
	/** The sql builder for this platform. */
	private SqlBuilder _builder;
	/** The model reader for this platform. */
	private JdbcModelReader _modelReader;
	/** Whether script mode is on. */
	private boolean _scriptModeOn;
	/** Whether SQL comments are generated or not. */
	private boolean _sqlCommentsOn = true;
	/** Whether delimited identifiers are used or not. */
	private boolean _delimitedIdentifierModeOn;
	/** Whether identity override is enabled. */
	private boolean _identityOverrideOn;
	/** Whether read foreign keys shall be sorted alphabetically. */
	private boolean _foreignKeysSorted;
	/**
	 * Whether to use the default ON UPDATE action if the specified one is
	 * unsupported.
	 */
	private boolean _useDefaultOnUpdateActionIfUnsupported = true;
	/**
	 * Whether to use the default ON DELETE action if the specified one is
	 * unsupported.
	 */
	private boolean _useDefaultOnDeleteActionIfUnsupported = true;

	/**
	 * {@inheritDoc}
	 */
	public SqlBuilder getSqlBuilder() {
		return _builder;
	}

	/**
	 * Sets the sql builder for this platform.
	 * 
	 * @param builder
	 *            The sql builder
	 */
	protected void setSqlBuilder(SqlBuilder builder) {
		_builder = builder;
	}

	/**
	 * {@inheritDoc}
	 */
	public JdbcModelReader getModelReader() {
		if (_modelReader == null) {
			_modelReader = new JdbcModelReader(this);
		}
		return _modelReader;
	}

	/**
	 * Sets the model reader for this platform.
	 * 
	 * @param modelReader
	 *            The model reader
	 */
	protected void setModelReader(JdbcModelReader modelReader) {
		_modelReader = modelReader;
	}

	/**
	 * {@inheritDoc}
	 */
	public PlatformInfo getPlatformInfo() {
		return _info;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isScriptModeOn() {
		return _scriptModeOn;
	}

	/**
	 * {@inheritDoc}
	 */
	public void setScriptModeOn(boolean scriptModeOn) {
		_scriptModeOn = scriptModeOn;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isSqlCommentsOn() {
		return _sqlCommentsOn;
	}

	/**
	 * {@inheritDoc}
	 */
	public void setSqlCommentsOn(boolean sqlCommentsOn) {
		if (!getPlatformInfo().isSqlCommentsSupported() && sqlCommentsOn) {
			throw new DdlUtilsException("Platform " + getName() + " does not support SQL comments");
		}
		_sqlCommentsOn = sqlCommentsOn;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isDelimitedIdentifierModeOn() {
		return _delimitedIdentifierModeOn;
	}

	/**
	 * {@inheritDoc}
	 */
	public void setDelimitedIdentifierModeOn(boolean delimitedIdentifierModeOn) {
		if (!getPlatformInfo().isDelimitedIdentifiersSupported() && delimitedIdentifierModeOn) {
			throw new DdlUtilsException("Platform " + getName() + " does not support delimited identifier");
		}
		_delimitedIdentifierModeOn = delimitedIdentifierModeOn;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isIdentityOverrideOn() {
		return _identityOverrideOn;
	}

	/**
	 * {@inheritDoc}
	 */
	public void setIdentityOverrideOn(boolean identityOverrideOn) {
		_identityOverrideOn = identityOverrideOn;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isForeignKeysSorted() {
		return _foreignKeysSorted;
	}

	/**
	 * {@inheritDoc}
	 */
	public void setForeignKeysSorted(boolean foreignKeysSorted) {
		_foreignKeysSorted = foreignKeysSorted;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isDefaultOnUpdateActionUsedIfUnsupported() {
		return _useDefaultOnUpdateActionIfUnsupported;
	}

	/**
	 * {@inheritDoc}
	 */
	public void setDefaultOnUpdateActionUsedIfUnsupported(boolean useDefault) {
		_useDefaultOnUpdateActionIfUnsupported = useDefault;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isDefaultOnDeleteActionUsedIfUnsupported() {
		return _useDefaultOnDeleteActionIfUnsupported;
	}

	/**
	 * {@inheritDoc}
	 */
	public void setDefaultOnDeleteActionUsedIfUnsupported(boolean useDefault) {
		_useDefaultOnDeleteActionIfUnsupported = useDefault;
	}

	/**
	 * Returns the log for this platform.
	 * 
	 * @return The log
	 */
	protected Log getLog() {
		return _log;
	}

	/**
	 * Logs any warnings associated to the given connection. Note that the
	 * connection needs to be open for this.
	 * 
	 * @param connection
	 *            The open connection
	 */
	protected void logWarnings(Connection connection) throws SQLException {
		SQLWarning warning = connection.getWarnings();

		while (warning != null) {
			getLog().warn(warning.getLocalizedMessage(), warning.getCause());
			warning = warning.getNextWarning();
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public int evaluateBatch(String sql, boolean continueOnError) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			return evaluateBatch(connection, sql, continueOnError);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public int evaluateBatch(Connection connection, String sql, boolean continueOnError)
			throws DatabaseOperationException {
		Statement statement = null;
		int errors = 0;
		int commandCount = 0;

		// we tokenize the SQL along the delimiters, and we also make sure that only
		// delimiters
		// at the end of a line or the end of the string are used (row mode)
		try {
			statement = connection.createStatement();

			SqlTokenizer tokenizer = new SqlTokenizer(sql);

			while (tokenizer.hasMoreStatements()) {
				String command = tokenizer.getNextStatement();

				// ignore whitespace
				command = command.trim();
				if (command.length() == 0) {
					continue;
				}

				commandCount++;

				if (_log.isDebugEnabled()) {
					_log.debug("About to execute SQL " + command);
				}
				try {
					int results = statement.executeUpdate(command);

					if (_log.isDebugEnabled()) {
						_log.debug("After execution, " + results + " row(s) have been changed");
					}
				} catch (SQLException ex) {
					if (continueOnError) {
						// Since the user deciced to ignore this error, we log the error
						// on level warn, and the exception itself on level debug
						_log.warn("SQL Command " + command + " failed with: " + ex.getMessage());
						if (_log.isDebugEnabled()) {
							_log.debug(ex);
						}
						errors++;
					} else {
						throw new DatabaseOperationException("Error while executing SQL " + command, ex);
					}
				}

				// lets display any warnings
				SQLWarning warning = connection.getWarnings();

				while (warning != null) {
					_log.warn(warning.toString());
					warning = warning.getNextWarning();
				}
				connection.clearWarnings();
			}
			_log.info("Executed " + commandCount + " SQL command(s) with " + errors + " error(s)");
		} catch (SQLException ex) {
			throw new DatabaseOperationException("Error while executing SQL", ex);
		} finally {
			closeStatement(statement);
		}

		return errors;
	}

	/**
	 * {@inheritDoc}
	 */
	public void shutdownDatabase() throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			shutdownDatabase(connection);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void shutdownDatabase(Connection connection) throws DatabaseOperationException {
		// Per default do nothing as most databases don't need this
	}

	/**
	 * {@inheritDoc}
	 */
	public void createDatabase(String jdbcDriverClassName, String connectionUrl, String username, String password,
			Map parameters) throws DatabaseOperationException, UnsupportedOperationException {
		throw new UnsupportedOperationException(
				"Database creation is not supported for the database platform " + getName());
	}

	/**
	 * {@inheritDoc}
	 */
	public void dropDatabase(String jdbcDriverClassName, String connectionUrl, String username, String password)
			throws DatabaseOperationException, UnsupportedOperationException {
		throw new UnsupportedOperationException(
				"Database deletion is not supported for the database platform " + getName());
	}

	/**
	 * {@inheritDoc}
	 */
	public void createTables(Database model, boolean dropTablesFirst, boolean continueOnError)
			throws DatabaseOperationException {
		createModel(model, dropTablesFirst, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public void createTables(Database model, CreationParameters params, boolean dropTablesFirst,
			boolean continueOnError) throws DatabaseOperationException {
		createModel(model, params, dropTablesFirst, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public void createTables(Connection connection, Database model, boolean dropTablesFirst, boolean continueOnError)
			throws DatabaseOperationException {
		createModel(connection, model, dropTablesFirst, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public void createTables(Connection connection, Database model, CreationParameters params, boolean dropTablesFirst,
			boolean continueOnError) throws DatabaseOperationException {
		createModel(connection, model, params, dropTablesFirst, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getCreateTablesSql(Database model, boolean dropTablesFirst, boolean continueOnError) {
		return getCreateModelSql(model, dropTablesFirst, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getCreateTablesSql(Database model, CreationParameters params, boolean dropTablesFirst,
			boolean continueOnError) {
		return getCreateModelSql(model, params, dropTablesFirst, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public void createModel(Database model, boolean dropTablesFirst, boolean continueOnError)
			throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			createModel(connection, model, dropTablesFirst, continueOnError);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void createModel(Connection connection, Database model, boolean dropTablesFirst, boolean continueOnError)
			throws DatabaseOperationException {
		String sql = getCreateModelSql(model, dropTablesFirst, continueOnError);

		evaluateBatch(connection, sql, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public void createModel(Database model, CreationParameters params, boolean dropTablesFirst, boolean continueOnError)
			throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			createModel(connection, model, params, dropTablesFirst, continueOnError);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void createModel(Connection connection, Database model, CreationParameters params, boolean dropTablesFirst,
			boolean continueOnError) throws DatabaseOperationException {
		String sql = getCreateModelSql(model, params, dropTablesFirst, continueOnError);

		evaluateBatch(connection, sql, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getCreateModelSql(Database model, boolean dropTablesFirst, boolean continueOnError) {
		String sql = null;

		try {
			StringWriter buffer = new StringWriter();

			getSqlBuilder().setWriter(buffer);
			getSqlBuilder().createTables(model, dropTablesFirst);
			sql = buffer.toString();
		} catch (IOException e) {
			// won't happen because we're using a string writer
		}
		return sql;
	}

	/**
	 * {@inheritDoc}
	 */
	public String getCreateModelSql(Database model, CreationParameters params, boolean dropTablesFirst,
			boolean continueOnError) {
		String sql = null;

		try {
			StringWriter buffer = new StringWriter();

			getSqlBuilder().setWriter(buffer);
			getSqlBuilder().createTables(model, params, dropTablesFirst);
			sql = buffer.toString();
		} catch (IOException e) {
			// won't happen because we're using a string writer
		}
		return sql;
	}

	/**
	 * Returns the model comparator to be used for this platform. This method is
	 * intendeded to be redefined by platforms that need to customize the model
	 * reader.
	 * 
	 * @return The model comparator
	 */
	protected ModelComparator getModelComparator() {
		return new ModelComparator(getPlatformInfo(), getTableDefinitionChangesPredicate(),
				isDelimitedIdentifierModeOn());
	}

	/**
	 * Returns the predicate that defines which changes are supported by the
	 * platform.
	 * 
	 * @return The predicate
	 */
	protected TableDefinitionChangesPredicate getTableDefinitionChangesPredicate() {
		return new DefaultTableDefinitionChangesPredicate();
	}

	/**
	 * {@inheritDoc}
	 */
	public List getChanges(Database currentModel, Database desiredModel) {
		List changes = getModelComparator().compare(currentModel, desiredModel);

		return sortChanges(changes);
	}

	/**
	 * Sorts the changes so that they can be executed by the database. E.g. tables
	 * need to be created before they can be referenced by foreign keys, indexes
	 * should be dropped before a table is dropped etc.
	 * 
	 * @param changes
	 *            The original changes
	 * @return The sorted changes - this can be the original list object or a new
	 *         one
	 */
	protected List sortChanges(List changes) {
		final Map, Integer> typeOrder = new HashMap<>();

		typeOrder.put(RemoveForeignKeyChange.class, new Integer(0));
		typeOrder.put(RemoveIndexChange.class, new Integer(1));
		typeOrder.put(RemoveTableChange.class, new Integer(2));
		typeOrder.put(RecreateTableChange.class, new Integer(3));
		typeOrder.put(RemovePrimaryKeyChange.class, new Integer(3));
		typeOrder.put(RemoveColumnChange.class, new Integer(4));
		typeOrder.put(ColumnDefinitionChange.class, new Integer(5));
		typeOrder.put(ColumnOrderChange.class, new Integer(5));
		typeOrder.put(AddColumnChange.class, new Integer(5));
		typeOrder.put(PrimaryKeyChange.class, new Integer(5));
		typeOrder.put(AddPrimaryKeyChange.class, new Integer(6));
		typeOrder.put(AddTableChange.class, new Integer(7));
		typeOrder.put(AddIndexChange.class, new Integer(8));
		typeOrder.put(AddForeignKeyChange.class, new Integer(9));

		Collections.sort(changes, new Comparator() {
			public int compare(ModelChange objA, ModelChange objB) {
				Integer orderValueA = (Integer) typeOrder.get(objA.getClass());
				Integer orderValueB = (Integer) typeOrder.get(objB.getClass());

				if (orderValueA == null) {
					return (orderValueB == null ? 0 : 1);
				} else if (orderValueB == null) {
					return -1;
				} else {
					return orderValueA.compareTo(orderValueB);
				}
			}
		});
		return changes;
	}

	/**
	 * {@inheritDoc}
	 */
	public void alterTables(Database desiredModel, boolean continueOnError) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			Database currentModel = readModelFromDatabase(connection, desiredModel.getName());

			alterModel(currentModel, desiredModel, continueOnError);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void alterTables(Database desiredModel, CreationParameters params, boolean continueOnError)
			throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			Database currentModel = readModelFromDatabase(connection, desiredModel.getName());

			alterModel(currentModel, desiredModel, params, continueOnError);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void alterTables(String catalog, String schema, String[] tableTypes, Database desiredModel,
			boolean continueOnError) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema,
					tableTypes);

			alterModel(currentModel, desiredModel, continueOnError);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void alterTables(String catalog, String schema, String[] tableTypes, Database desiredModel,
			CreationParameters params, boolean continueOnError) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema,
					tableTypes);

			alterModel(currentModel, desiredModel, params, continueOnError);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void alterTables(Connection connection, Database desiredModel, boolean continueOnError)
			throws DatabaseOperationException {
		Database currentModel = readModelFromDatabase(connection, desiredModel.getName());

		alterModel(currentModel, desiredModel, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public void alterTables(Connection connection, Database desiredModel, CreationParameters params,
			boolean continueOnError) throws DatabaseOperationException {
		Database currentModel = readModelFromDatabase(connection, desiredModel.getName());

		alterModel(currentModel, desiredModel, params, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public void alterTables(Connection connection, String catalog, String schema, String[] tableTypes,
			Database desiredModel, boolean continueOnError) throws DatabaseOperationException {
		Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema, tableTypes);

		alterModel(currentModel, desiredModel, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public void alterTables(Connection connection, String catalog, String schema, String[] tableTypes,
			Database desiredModel, CreationParameters params, boolean continueOnError)
			throws DatabaseOperationException {
		Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema, tableTypes);

		alterModel(currentModel, desiredModel, params, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getAlterTablesSql(Database desiredModel) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			Database currentModel = readModelFromDatabase(connection, desiredModel.getName());

			return getAlterModelSql(currentModel, desiredModel);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public String getAlterTablesSql(Database desiredModel, CreationParameters params)
			throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			Database currentModel = readModelFromDatabase(connection, desiredModel.getName());

			return getAlterModelSql(currentModel, desiredModel, params);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public String getAlterTablesSql(String catalog, String schema, String[] tableTypes, Database desiredModel)
			throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema,
					tableTypes);

			return getAlterModelSql(currentModel, desiredModel);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public String getAlterTablesSql(String catalog, String schema, String[] tableTypes, Database desiredModel,
			CreationParameters params) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema,
					tableTypes);

			return getAlterModelSql(currentModel, desiredModel, params);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public String getAlterTablesSql(Connection connection, Database desiredModel) throws DatabaseOperationException {
		Database currentModel = readModelFromDatabase(connection, desiredModel.getName());

		return getAlterModelSql(currentModel, desiredModel);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getAlterTablesSql(Connection connection, Database desiredModel, CreationParameters params)
			throws DatabaseOperationException {
		Database currentModel = readModelFromDatabase(connection, desiredModel.getName());

		return getAlterModelSql(currentModel, desiredModel, params);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getAlterTablesSql(Connection connection, String catalog, String schema, String[] tableTypes,
			Database desiredModel) throws DatabaseOperationException {
		Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema, tableTypes);

		return getAlterModelSql(currentModel, desiredModel);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getAlterTablesSql(Connection connection, String catalog, String schema, String[] tableTypes,
			Database desiredModel, CreationParameters params) throws DatabaseOperationException {
		Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema, tableTypes);

		return getAlterModelSql(currentModel, desiredModel, params);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getAlterModelSql(Database currentModel, Database desiredModel) throws DatabaseOperationException {
		return getAlterModelSql(currentModel, desiredModel, null);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getAlterModelSql(Database currentModel, Database desiredModel, CreationParameters params)
			throws DatabaseOperationException {
		List changes = getChanges(currentModel, desiredModel);
		String sql = null;

		try {
			StringWriter buffer = new StringWriter();

			getSqlBuilder().setWriter(buffer);
			processChanges(currentModel, changes, params);
			sql = buffer.toString();
		} catch (IOException ex) {
			// won't happen because we're using a string writer
		}
		return sql;
	}

	/**
	 * {@inheritDoc}
	 */
	public void alterModel(Database currentModel, Database desiredModel, boolean continueOnError)
			throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			alterModel(connection, currentModel, desiredModel, continueOnError);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void alterModel(Database currentModel, Database desiredModel, CreationParameters params,
			boolean continueOnError) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			alterModel(connection, currentModel, desiredModel, params, continueOnError);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void alterModel(Connection connection, Database currentModel, Database desiredModel, boolean continueOnError)
			throws DatabaseOperationException {
		String sql = getAlterModelSql(currentModel, desiredModel);
		_log.debug("sql");
	
		evaluateBatch(connection, sql, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public void alterModel(Connection connection, Database currentModel, Database desiredModel,
			CreationParameters params, boolean continueOnError) throws DatabaseOperationException {
		String sql = getAlterModelSql(currentModel, desiredModel, params);

		evaluateBatch(connection, sql, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public void dropTable(Connection connection, Database model, Table table, boolean continueOnError)
			throws DatabaseOperationException {
		String sql = getDropTableSql(model, table, continueOnError);

		evaluateBatch(connection, sql, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public void dropTable(Database model, Table table, boolean continueOnError) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			dropTable(connection, model, table, continueOnError);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public String getDropTableSql(Database model, Table table, boolean continueOnError) {
		String sql = null;

		try {
			StringWriter buffer = new StringWriter();

			getSqlBuilder().setWriter(buffer);
			getSqlBuilder().dropTable(model, table);
			sql = buffer.toString();
		} catch (IOException e) {
			// won't happen because we're using a string writer
		}
		return sql;
	}

	/**
	 * {@inheritDoc}
	 */
	public void dropTables(Database model, boolean continueOnError) throws DatabaseOperationException {
		dropModel(model, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public void dropTables(Connection connection, Database model, boolean continueOnError)
			throws DatabaseOperationException {
		dropModel(connection, model, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getDropTablesSql(Database model, boolean continueOnError) {
		return getDropModelSql(model);
	}

	/**
	 * {@inheritDoc}
	 */
	public void dropModel(Database model, boolean continueOnError) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			dropModel(connection, model, continueOnError);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void dropModel(Connection connection, Database model, boolean continueOnError)
			throws DatabaseOperationException {
		String sql = getDropModelSql(model);

		evaluateBatch(connection, sql, continueOnError);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getDropModelSql(Database model) {
		String sql = null;

		try {
			StringWriter buffer = new StringWriter();

			getSqlBuilder().setWriter(buffer);
			getSqlBuilder().dropTables(model);
			sql = buffer.toString();
		} catch (IOException e) {
			// won't happen because we're using a string writer
		}
		return sql;
	}

	/**
	 * Processes the given changes in the specified order. Basically, this method
	 * finds the appropriate handler method (one of the processChange
	 * methods) defined in the concrete sql builder for each change, and invokes it.
	 * 
	 * @param model
	 *            The database model; this object is not going to be changed by this
	 *            method
	 * @param changes
	 *            The changes
	 * @param params
	 *            The parameters used in the creation of new tables. Note that for
	 *            existing tables, the parameters won't be applied
	 * @return The changed database model
	 */
	protected Database processChanges(Database model, Collection changes, CreationParameters params)
			throws IOException, DdlUtilsException {
		Database currentModel = new CloneHelper().clone(model);

		for (Iterator it = changes.iterator(); it.hasNext();) {
			invokeChangeHandler(currentModel, params, it.next());
		}
		return currentModel;
	}

	/**
	 * Invokes the change handler (one of the processChange methods)
	 * for the given change object.
	 * 
	 * @param currentModel
	 *            The current database schema
	 * @param params
	 *            The parameters used in the creation of new tables. Note that for
	 *            existing tables, the parameters won't be applied
	 * @param change
	 *            The change object
	 */
	private void invokeChangeHandler(Database currentModel, CreationParameters params, ModelChange change)
			throws IOException {
		Class curClass = getClass();

		// find the handler for the change
		while ((curClass != null) && !Object.class.equals(curClass)) {
			try {
				Method method = null;

				try {
					method = curClass.getDeclaredMethod("processChange",
							new Class[] { Database.class, CreationParameters.class, change.getClass() });
				} catch (NoSuchMethodException ex) {
					// we actually expect this one
				}

				if (method != null) {
					method.invoke(this, new Object[] { currentModel, params, change });
					return;
				} else {
					curClass = curClass.getSuperclass();
				}
			} catch (InvocationTargetException ex) {
				if (ex.getTargetException() instanceof IOException) {
					throw (IOException) ex.getTargetException();
				} else {
					throw new DdlUtilsException(ex.getTargetException());
				}
			} catch (Exception ex) {
				throw new DdlUtilsException(ex);
			}
		}
		throw new DdlUtilsException("No handler for change of type " + change.getClass().getName() + " defined");
	}

	/**
	 * Finds the table changed by the change object in the given model.
	 * 
	 * @param currentModel
	 *            The model to find the table in
	 * @param change
	 *            The table change
	 * @return The table
	 * @throws ModelException
	 *             If the table could not be found
	 */
	protected Table findChangedTable(Database currentModel, TableChange change) throws ModelException {
		Table table = currentModel.findTable(change.getChangedTable(),
				getPlatformInfo().isDelimitedIdentifiersSupported());

		if (table == null) {
			throw new ModelException("Could not find table " + change.getChangedTable() + " in the given model");
		} else {
			return table;
		}
	}

	/**
	 * Finds the index changed by the change object in the given model.
	 * 
	 * @param currentModel
	 *            The model to find the index in
	 * @param change
	 *            The index change
	 * @return The index
	 * @throws ModelException
	 *             If the index could not be found
	 */
	protected Index findChangedIndex(Database currentModel, IndexChange change) throws ModelException {
		Index index = change.findChangedIndex(currentModel, getPlatformInfo().isDelimitedIdentifiersSupported());

		if (index == null) {
			throw new ModelException(
					"Could not find the index to change in table " + change.getChangedTable() + " in the given model");
		} else {
			return index;
		}
	}

	/**
	 * Finds the foreign key changed by the change object in the given model.
	 * 
	 * @param currentModel
	 *            The model to find the foreign key in
	 * @param change
	 *            The foreign key change
	 * @return The foreign key
	 * @throws ModelException
	 *             If the foreign key could not be found
	 */
	protected ForeignKey findChangedForeignKey(Database currentModel, ForeignKeyChange change) throws ModelException {
		ForeignKey fk = change.findChangedForeignKey(currentModel, getPlatformInfo().isDelimitedIdentifiersSupported());

		if (fk == null) {
			throw new ModelException("Could not find the foreign key to change in table " + change.getChangedTable()
					+ " in the given model");
		} else {
			return fk;
		}
	}

	/**
	 * Processes a change representing the addition of a table.
	 * 
	 * @param currentModel
	 *            The current database schema
	 * @param params
	 *            The parameters used in the creation of new tables. Note that for
	 *            existing tables, the parameters won't be applied
	 * @param change
	 *            The change object
	 * @throws IOException
	 */
	public void processChange(Database currentModel, CreationParameters params, AddTableChange change)
			throws IOException {
		getSqlBuilder().createTable(currentModel, change.getNewTable(),
				params == null ? null : params.getParametersFor(change.getNewTable()));
		change.apply(currentModel, isDelimitedIdentifierModeOn());
	}

	/**
	 * Processes a change representing the removal of a table.
	 * 
	 * @param currentModel
	 *            The current database schema
	 * @param params
	 *            The parameters used in the creation of new tables. Note that for
	 *            existing tables, the parameters won't be applied
	 * @param change
	 *            The change object
	 * @throws IOException
	 * @throws ModelException
	 */
	public void processChange(Database currentModel, CreationParameters params, RemoveTableChange change)
			throws IOException, ModelException {
		Table changedTable = findChangedTable(currentModel, change);

		getSqlBuilder().dropTable(changedTable);
		change.apply(currentModel, isDelimitedIdentifierModeOn());
	}

	/**
	 * Processes a change representing the addition of a foreign key.
	 * 
	 * @param currentModel
	 *            The current database schema
	 * @param params
	 *            The parameters used in the creation of new tables. Note that for
	 *            existing tables, the parameters won't be applied
	 * @param change
	 *            The change object
	 * @throws IOException
	 */
	public void processChange(Database currentModel, CreationParameters params, AddForeignKeyChange change)
			throws IOException {
		Table changedTable = findChangedTable(currentModel, change);

		getSqlBuilder().createForeignKey(currentModel, changedTable, change.getNewForeignKey());
		change.apply(currentModel, isDelimitedIdentifierModeOn());
	}

	/**
	 * Processes a change representing the removal of a foreign key.
	 * 
	 * @param currentModel
	 *            The current database schema
	 * @param params
	 *            The parameters used in the creation of new tables. Note that for
	 *            existing tables, the parameters won't be applied
	 * @param change
	 *            The change object
	 * @throws IOException
	 * @throws ModelException
	 */
	public void processChange(Database currentModel, CreationParameters params, RemoveForeignKeyChange change)
			throws IOException, ModelException {
		Table changedTable = findChangedTable(currentModel, change);
		ForeignKey changedFk = findChangedForeignKey(currentModel, change);

		getSqlBuilder().dropForeignKey(changedTable, changedFk);
		change.apply(currentModel, isDelimitedIdentifierModeOn());
	}

	/**
	 * Processes a change representing the addition of an index.
	 * 
	 * @param currentModel
	 *            The current database schema
	 * @param params
	 *            The parameters used in the creation of new tables. Note that for
	 *            existing tables, the parameters won't be applied
	 * @param change
	 *            The change object
	 * @throws IOException
	 */
	public void processChange(Database currentModel, CreationParameters params, AddIndexChange change)
			throws IOException {
		Table changedTable = findChangedTable(currentModel, change);

		getSqlBuilder().createIndex(changedTable, change.getNewIndex());
		change.apply(currentModel, isDelimitedIdentifierModeOn());
	}

	/**
	 * Processes a change representing the removal of an index.
	 * 
	 * @param currentModel
	 *            The current database schema
	 * @param params
	 *            The parameters used in the creation of new tables. Note that for
	 *            existing tables, the parameters won't be applied
	 * @param change
	 *            The change object
	 * @throws IOException
	 * @throws ModelException
	 */
	public void processChange(Database currentModel, CreationParameters params, RemoveIndexChange change)
			throws IOException, ModelException {
		Table changedTable = findChangedTable(currentModel, change);
		Index changedIndex = findChangedIndex(currentModel, change);

		getSqlBuilder().dropIndex(changedTable, changedIndex);
		change.apply(currentModel, isDelimitedIdentifierModeOn());
	}

	/**
	 * Processes a change representing the addition of a column.
	 * 
	 * @param currentModel
	 *            The current database schema
	 * @param params
	 *            The parameters used in the creation of new tables. Note that for
	 *            existing tables, the parameters won't be applied
	 * @param change
	 *            The change object
	 * @throws IOException
	 */
	public void processChange(Database currentModel, CreationParameters params, AddColumnChange change)
			throws IOException {
		Table changedTable = findChangedTable(currentModel, change);

		getSqlBuilder().addColumn(currentModel, changedTable, change.getNewColumn());
		change.apply(currentModel, isDelimitedIdentifierModeOn());
	}

	/**
	 * Processes a change representing the addition of a primary key.
	 * 
	 * @param currentModel
	 *            The current database schema
	 * @param params
	 *            The parameters used in the creation of new tables. Note that for
	 *            existing tables, the parameters won't be applied
	 * @param change
	 *            The change object
	 * @throws IOException
	 */
	public void processChange(Database currentModel, CreationParameters params, AddPrimaryKeyChange change)
			throws IOException {
		Table changedTable = findChangedTable(currentModel, change);
		String[] pkColumnNames = change.getPrimaryKeyColumns();
		Column[] pkColumns = new Column[pkColumnNames.length];

		for (int colIdx = 0; colIdx < pkColumns.length; colIdx++) {
			pkColumns[colIdx] = changedTable.findColumn(pkColumnNames[colIdx], isDelimitedIdentifierModeOn());
		}
		getSqlBuilder().createPrimaryKey(changedTable, pkColumns);
		change.apply(currentModel, isDelimitedIdentifierModeOn());
	}

	/**
	 * Processes a change representing the recreation of a table.
	 * 
	 * @param currentModel
	 *            The current database schema
	 * @param params
	 *            The parameters used in the creation of new tables. Note that for
	 *            existing tables, the parameters won't be applied
	 * @param change
	 *            The change object
	 * @throws IOException
	 */
	public void processChange(Database currentModel, CreationParameters params, RecreateTableChange change)
			throws IOException {
		// we can only copy the data if no required columns without default value and
		// non-autoincrement have been added
		boolean canMigrateData = true;

		for (Iterator it = change.getOriginalChanges().iterator(); canMigrateData && it.hasNext();) {
			TableChange curChange = (TableChange) it.next();

			if (curChange instanceof AddColumnChange) {
				AddColumnChange addColumnChange = (AddColumnChange) curChange;

				if (addColumnChange.getNewColumn().isRequired() && !addColumnChange.getNewColumn().isAutoIncrement()
						&& (addColumnChange.getNewColumn().getDefaultValue() == null)) {
					_log.warn("Data cannot be retained in table " + change.getChangedTable()
							+ " because of the addition of the required column "
							+ addColumnChange.getNewColumn().getName());
					canMigrateData = false;
				}
			}
		}

		Table changedTable = findChangedTable(currentModel, change);
		Table targetTable = change.getTargetTable();
		Map parameters = (params == null ? null : params.getParametersFor(targetTable));

		if (canMigrateData) {
			Table tempTable = getTemporaryTableFor(targetTable);

			getSqlBuilder().createTemporaryTable(currentModel, tempTable, parameters);
			getSqlBuilder().copyData(changedTable, tempTable);
			// Note that we don't drop the indices here because the DROP TABLE will take
			// care of that
			// Likewise, foreign keys have already been dropped as necessary
			getSqlBuilder().dropTable(changedTable);
			getSqlBuilder().createTable(currentModel, targetTable, parameters);
			getSqlBuilder().copyData(tempTable, targetTable);
			getSqlBuilder().dropTemporaryTable(currentModel, tempTable);
		} else {
			getSqlBuilder().dropTable(changedTable);
			getSqlBuilder().createTable(currentModel, targetTable, parameters);
		}

		change.apply(currentModel, isDelimitedIdentifierModeOn());
	}

	/**
	 * Creates a temporary table object that corresponds to the given table.
	 * Database-specific implementations may redefine this method if e.g. the
	 * database directly supports temporary tables. The default implementation
	 * simply appends an underscore to the table name and uses that as the table
	 * name.
	 * 
	 * @param targetTable
	 *            The target table
	 * @return The temporary table
	 */
	protected Table getTemporaryTableFor(Table targetTable) {
		CloneHelper cloneHelper = new CloneHelper();
		Table table = new Table();

		table.setCatalog(targetTable.getCatalog());
		table.setSchema(targetTable.getSchema());
		table.setName(targetTable.getName() + "_");
		table.setType(targetTable.getType());
		for (int idx = 0; idx < targetTable.getColumnCount(); idx++) {
			// TODO: clone PK status ?
			table.addColumn(cloneHelper.clone(targetTable.getColumn(idx), true));
		}

		return table;
	}

	/**
	 * {@inheritDoc}
	 */
	public Iterator query(Database model, String sql) throws DatabaseOperationException {
		return query(model, sql, (Table[]) null);
	}

	/**
	 * {@inheritDoc}
	 */
	public Iterator query(Database model, String sql, Collection parameters) throws DatabaseOperationException {
		return query(model, sql, parameters, null);
	}

	/**
	 * {@inheritDoc}
	 */
	public Iterator query(Database model, String sql, Table[] queryHints) throws DatabaseOperationException {
		Connection connection = borrowConnection();
		Statement statement = null;
		ResultSet resultSet = null;
		Iterator answer = null;

		try {
			statement = connection.createStatement();
			resultSet = statement.executeQuery(sql);
			answer = createResultSetIterator(model, resultSet, queryHints);
			return answer;
		} catch (SQLException ex) {
			throw new DatabaseOperationException("Error while performing a query", ex);
		} finally {
			// if any exceptions are thrown, close things down
			// otherwise we're leaving it open for the iterator
			if (answer == null) {
				closeStatement(statement);
				returnConnection(connection);
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public Iterator query(Database model, String sql, Collection parameters, Table[] queryHints)
			throws DatabaseOperationException {
		Connection connection = borrowConnection();
		PreparedStatement statement = null;
		ResultSet resultSet = null;
		Iterator answer = null;

		try {
			statement = connection.prepareStatement(sql);

			int paramIdx = 1;

			for (Iterator iter = parameters.iterator(); iter.hasNext(); paramIdx++) {
				Object arg = iter.next();

				if (arg instanceof BigDecimal) {
					// to avoid scale problems because setObject assumes a scale of 0
					statement.setBigDecimal(paramIdx, (BigDecimal) arg);
				} else {
					statement.setObject(paramIdx, arg);
				}
			}
			resultSet = statement.executeQuery();
			answer = createResultSetIterator(model, resultSet, queryHints);
			return answer;
		} catch (SQLException ex) {
			throw new DatabaseOperationException("Error while performing a query", ex);
		} finally {
			// if any exceptions are thrown, close things down
			// otherwise we're leaving it open for the iterator
			if (answer == null) {
				closeStatement(statement);
				returnConnection(connection);
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public List fetch(Database model, String sql) throws DatabaseOperationException {
		return fetch(model, sql, (Table[]) null, 0, -1);
	}

	/**
	 * {@inheritDoc}
	 */
	public List fetch(Database model, String sql, Table[] queryHints) throws DatabaseOperationException {
		return fetch(model, sql, queryHints, 0, -1);
	}

	/**
	 * {@inheritDoc}
	 */
	public List fetch(Database model, String sql, int start, int end) throws DatabaseOperationException {
		return fetch(model, sql, (Table[]) null, start, end);
	}

	/**
	 * {@inheritDoc}
	 */
	public List fetch(Database model, String sql, Table[] queryHints, int start, int end)
			throws DatabaseOperationException {
		Connection connection = borrowConnection();
		Statement statement = null;
		ResultSet resultSet = null;
		List result = new ArrayList<>();

		try {
			statement = connection.createStatement();
			resultSet = statement.executeQuery(sql);

			int rowIdx = 0;

			for (ModelBasedResultSetIterator it = createResultSetIterator(model, resultSet, queryHints); ((end < 0)
					|| (rowIdx <= end)) && it.hasNext(); rowIdx++) {
				if (rowIdx >= start) {
					result.add(it.next());
				} else {
					it.advance();
				}
			}
		} catch (SQLException ex) {
			throw new DatabaseOperationException("Error while fetching data from the database", ex);
		} finally {
			// the iterator should return the connection automatically
			// so this is usually not necessary (but just in case)
			closeStatement(statement);
			returnConnection(connection);
		}
		return result;
	}

	/**
	 * {@inheritDoc}
	 */
	public List fetch(Database model, String sql, Collection parameters) throws DatabaseOperationException {
		return fetch(model, sql, parameters, null, 0, -1);
	}

	/**
	 * {@inheritDoc}
	 */
	public List fetch(Database model, String sql, Collection parameters, int start, int end)
			throws DatabaseOperationException {
		return fetch(model, sql, parameters, null, start, end);
	}

	/**
	 * {@inheritDoc}
	 */
	public List fetch(Database model, String sql, Collection parameters, Table[] queryHints)
			throws DatabaseOperationException {
		return fetch(model, sql, parameters, queryHints, 0, -1);
	}

	/**
	 * {@inheritDoc}
	 */
	public List fetch(Database model, String sql, Collection parameters, Table[] queryHints, int start, int end)
			throws DatabaseOperationException {
		Connection connection = borrowConnection();
		PreparedStatement statement = null;
		ResultSet resultSet = null;
		List result = new ArrayList<>();

		try {
			statement = connection.prepareStatement(sql);

			int paramIdx = 1;

			for (Iterator iter = parameters.iterator(); iter.hasNext(); paramIdx++) {
				Object arg = iter.next();

				if (arg instanceof BigDecimal) {
					// to avoid scale problems because setObject assumes a scale of 0
					statement.setBigDecimal(paramIdx, (BigDecimal) arg);
				} else {
					statement.setObject(paramIdx, arg);
				}
			}
			resultSet = statement.executeQuery();

			int rowIdx = 0;

			for (ModelBasedResultSetIterator it = createResultSetIterator(model, resultSet, queryHints); ((end < 0)
					|| (rowIdx <= end)) && it.hasNext(); rowIdx++) {
				if (rowIdx >= start) {
					result.add(it.next());
				} else {
					it.advance();
				}
			}
		} catch (SQLException ex) {
			// any other exception comes from the iterator which closes the resources
			// automatically
			closeStatement(statement);
			returnConnection(connection);
			throw new DatabaseOperationException("Error while fetching data from the database", ex);
		}
		return result;
	}

	/**
	 * Creates the SQL for inserting an object of the given type. If a concrete bean
	 * is given, then a concrete insert statement is created, otherwise an insert
	 * statement usable in a prepared statement is build.
	 *
	 * @param model
	 *            The database model
	 * @param dynaClass
	 *            The type
	 * @param properties
	 *            The properties to write
	 * @param bean
	 *            Optionally the concrete bean to insert
	 * @return The SQL required to insert an instance of the class
	 */
	protected String createInsertSql(Database model, SqlDynaClass dynaClass, SqlDynaProperty[] properties,
			DynaBean bean) {
		Table table = model.findTable(dynaClass.getTableName());
		HashMap columnValues = toColumnValues(properties, bean);

		return _builder.getInsertSql(table, columnValues, bean == null);
	}

	/**
	 * Creates the SQL for querying for the id generated by the last insert of an
	 * object of the given type.
	 * 
	 * @param model
	 *            The database model
	 * @param dynaClass
	 *            The type
	 * @return The SQL required for querying for the id, or null if the
	 *         database does not support this
	 */
	protected String createSelectLastInsertIdSql(Database model, SqlDynaClass dynaClass) {
		Table table = model.findTable(dynaClass.getTableName());

		return _builder.getSelectLastIdentityValues(table);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getInsertSql(Database model, DynaBean dynaBean) {
		SqlDynaClass dynaClass = model.getDynaClassFor(dynaBean);
		SqlDynaProperty[] properties = dynaClass.getSqlDynaProperties();

		if (properties.length == 0) {
			_log.info("Cannot insert instances of type " + dynaClass + " because it has no properties");
			return null;
		}

		return createInsertSql(model, dynaClass, properties, dynaBean);
	}

	/**
	 * Returns all properties where the column is not non-autoincrement and for
	 * which the bean either has a value or the column hasn't got a default value,
	 * for the given dyna class.
	 * 
	 * @param model
	 *            The database model
	 * @param dynaClass
	 *            The dyna class
	 * @param bean
	 *            The bean
	 * @return The properties
	 */
	private SqlDynaProperty[] getPropertiesForInsertion(Database model, SqlDynaClass dynaClass, final DynaBean bean) {
		SqlDynaProperty[] properties = dynaClass.getSqlDynaProperties();

		@SuppressWarnings("unchecked")
		Collection result = CollectionUtils.select(Arrays.asList(properties), new Predicate() {
			public boolean evaluate(Object input) {
				SqlDynaProperty prop = (SqlDynaProperty) input;

				if (bean.get(prop.getName()) != null) {
					// we ignore properties for which a value is present in the bean
					// only if they are identity and identity override is off or
					// the platform does not allow the override of the auto-increment
					// specification
					return !prop.getColumn().isAutoIncrement()
							|| (isIdentityOverrideOn() && getPlatformInfo().isIdentityOverrideAllowed());
				} else {
					// we also return properties without a value in the bean
					// if they ain't auto-increment and don't have a default value
					// in this case, a NULL is inserted
					return !prop.getColumn().isAutoIncrement() && (prop.getColumn().getDefaultValue() == null);
				}
			}
		});

		return (SqlDynaProperty[]) result.toArray(new SqlDynaProperty[result.size()]);
	}

	/**
	 * Returns all identity properties whose value were defined by the database and
	 * which now need to be read back from the DB.
	 * 
	 * @param model
	 *            The database model
	 * @param dynaClass
	 *            The dyna class
	 * @param bean
	 *            The bean
	 * @return The columns
	 */
	private Column[] getRelevantIdentityColumns(Database model, SqlDynaClass dynaClass, final DynaBean bean) {
		SqlDynaProperty[] properties = dynaClass.getSqlDynaProperties();

		@SuppressWarnings("unchecked")
		Collection relevantProperties = CollectionUtils.select(Arrays.asList(properties),
				new Predicate() {
					public boolean evaluate(Object input) {
						SqlDynaProperty prop = (SqlDynaProperty) input;

						// we only want those identity columns that were really specified by the DB
						// if the platform allows specification of values for identity columns
						// in INSERT/UPDATE statements, then we need to filter the corresponding
						// columns out
						return prop.getColumn().isAutoIncrement()
								&& (!isIdentityOverrideOn() || !getPlatformInfo().isIdentityOverrideAllowed()
										|| (bean.get(prop.getName()) == null));
					}
				});

		Column[] columns = new Column[relevantProperties.size()];
		int idx = 0;

		for (Iterator propIt = relevantProperties.iterator(); propIt.hasNext(); idx++) {
			columns[idx] = ((SqlDynaProperty) propIt.next()).getColumn();
		}
		return columns;
	}

	/**
	 * {@inheritDoc}
	 */
	public void insert(Connection connection, Database model, DynaBean dynaBean) throws DatabaseOperationException {
		SqlDynaClass dynaClass = model.getDynaClassFor(dynaBean);
		SqlDynaProperty[] properties = getPropertiesForInsertion(model, dynaClass, dynaBean);
		Column[] autoIncrColumns = getRelevantIdentityColumns(model, dynaClass, dynaBean);

		if ((properties.length == 0) && (autoIncrColumns.length == 0)) {
			_log.warn("Cannot insert instances of type " + dynaClass + " because it has no usable properties");
			return;
		}

		String insertSql = createInsertSql(model, dynaClass, properties, null);
		String queryIdentitySql = null;

		if (_log.isDebugEnabled()) {
			_log.debug("About to execute SQL: " + insertSql);
		}

		if (autoIncrColumns.length > 0) {
			if (!getPlatformInfo().isLastIdentityValueReadable()) {
				_log.warn("The database does not support querying for auto-generated column values");
			} else {
				queryIdentitySql = createSelectLastInsertIdSql(model, dynaClass);
			}
		}

		boolean autoCommitMode = false;
		PreparedStatement statement = null;

		try {
			if (!getPlatformInfo().isAutoCommitModeForLastIdentityValueReading()) {
				autoCommitMode = connection.getAutoCommit();
				connection.setAutoCommit(false);
			}

			beforeInsert(connection, dynaClass.getTable());

			statement = connection.prepareStatement(insertSql);

			for (int idx = 0; idx < properties.length; idx++) {
				setObject(statement, idx + 1, dynaBean, properties[idx]);
			}

			int count = statement.executeUpdate();

			afterInsert(connection, dynaClass.getTable());

			if (count != 1) {
				_log.warn("Attempted to insert a single row " + dynaBean + " in table " + dynaClass.getTableName()
						+ " but changed " + count + " row(s)");
			}
		} catch (SQLException ex) {
			throw new DatabaseOperationException("Error while inserting into the database: " + ex.getMessage(), ex);
		} finally {
			closeStatement(statement);
		}
		if (queryIdentitySql != null) {
			Statement queryStmt = null;
			ResultSet lastInsertedIds = null;

			try {
				if (getPlatformInfo().isAutoCommitModeForLastIdentityValueReading()) {
					// we'll commit the statement(s) if no auto-commit is enabled because
					// otherwise it is possible that the auto increment hasn't happened yet
					// (the db didn't actually perform the insert yet so no triggering of
					// sequences did occur)
					if (!connection.getAutoCommit()) {
						connection.commit();
					}
				}

				queryStmt = connection.createStatement();
				lastInsertedIds = queryStmt.executeQuery(queryIdentitySql);

				lastInsertedIds.next();

				for (int idx = 0; idx < autoIncrColumns.length; idx++) {
					// we're using the index rather than the name because we cannot know how
					// the SQL statement looks like; rather we assume that we get the values
					// back in the same order as the auto increment columns
					Object value = getObjectFromResultSet(lastInsertedIds, autoIncrColumns[idx], idx + 1);

					PropertyUtils.setProperty(dynaBean, autoIncrColumns[idx].getName(), value);
				}
			} catch (NoSuchMethodException ex) {
				// Can't happen because we're using dyna beans
			} catch (IllegalAccessException ex) {
				// Can't happen because we're using dyna beans
			} catch (InvocationTargetException ex) {
				// Can't happen because we're using dyna beans
			} catch (SQLException ex) {
				throw new DatabaseOperationException(
						"Error while retrieving the identity column value(s) from the database", ex);
			} finally {
				if (lastInsertedIds != null) {
					try {
						lastInsertedIds.close();
					} catch (SQLException ex) {
						// we ignore this one
					}
				}
				closeStatement(statement);
			}
		}
		if (!getPlatformInfo().isAutoCommitModeForLastIdentityValueReading()) {
			try {
				// we need to do a manual commit now
				connection.commit();
				connection.setAutoCommit(autoCommitMode);
			} catch (SQLException ex) {
				throw new DatabaseOperationException(ex);
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void insert(Database model, DynaBean dynaBean) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			insert(connection, model, dynaBean);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void insert(Connection connection, Database model, Collection dynaBeans)
			throws DatabaseOperationException {
		SqlDynaClass dynaClass = null;
		SqlDynaProperty[] properties = null;
		PreparedStatement statement = null;
		int addedStmts = 0;
		boolean identityWarningPrinted = false;

		for (Iterator it = dynaBeans.iterator(); it.hasNext();) {
			DynaBean dynaBean = it.next();
			SqlDynaClass curDynaClass = model.getDynaClassFor(dynaBean);

			if (curDynaClass != dynaClass) {
				if (dynaClass != null) {
					executeBatch(statement, addedStmts, dynaClass.getTable());
					addedStmts = 0;
				}

				dynaClass = curDynaClass;
				properties = getPropertiesForInsertion(model, curDynaClass, dynaBean);

				if (properties.length == 0) {
					_log.warn("Cannot insert instances of type " + dynaClass + " because it has no usable properties");
					continue;
				}
				if (!identityWarningPrinted && (getRelevantIdentityColumns(model, curDynaClass, dynaBean).length > 0)) {
					_log.warn(
							"Updating the bean properties corresponding to auto-increment columns is not supported in batch mode");
					identityWarningPrinted = true;
				}

				String insertSql = createInsertSql(model, dynaClass, properties, null);

				if (_log.isDebugEnabled()) {
					_log.debug("Starting new batch with SQL: " + insertSql);
				}
				try {
					statement = connection.prepareStatement(insertSql);
				} catch (SQLException ex) {
					throw new DatabaseOperationException("Error while preparing insert statement", ex);
				}
			}
			try {
				if (properties != null) {
					for (int idx = 0; idx < properties.length; idx++) {
						setObject(statement, idx + 1, dynaBean, properties[idx]);
					}
					if (statement != null) {
						statement.addBatch();
						addedStmts++;
					}
				}
			} catch (SQLException ex) {
				throw new DatabaseOperationException("Error while adding batch insert", ex);
			}
		}
		if (dynaClass != null) {
			executeBatch(statement, addedStmts, dynaClass.getTable());
		}
	}

	/**
	 * Performs the batch for the given statement, and checks that the specified
	 * amount of rows have been changed.
	 * 
	 * @param statement
	 *            The prepared statement
	 * @param numRows
	 *            The number of rows that should change
	 * @param table
	 *            The changed table
	 */
	private void executeBatch(PreparedStatement statement, int numRows, Table table) throws DatabaseOperationException {
		if (statement != null) {
			try {
				Connection connection = statement.getConnection();

				beforeInsert(connection, table);

				int[] results = statement.executeBatch();

				closeStatement(statement);
				afterInsert(connection, table);

				boolean hasSum = true;
				int sum = 0;

				for (int idx = 0; (results != null) && (idx < results.length); idx++) {
					if (results[idx] < 0) {
						hasSum = false;
						if (results[idx] == Statement.EXECUTE_FAILED) {
							_log.warn("The batch insertion of row " + idx + " into table " + table.getName()
									+ " failed but the driver is able to continue processing");
						} else if (results[idx] != Statement.SUCCESS_NO_INFO) {
							_log.warn("The batch insertion of row " + idx + " into table " + table.getName()
									+ " returned an undefined status value " + results[idx]);
						}
					} else {
						sum += results[idx];
					}
				}
				if (hasSum && (sum != numRows)) {
					_log.warn("Attempted to insert " + numRows + " rows into table " + table.getName() + " but changed "
							+ sum + " rows");
				}
			} catch (SQLException ex) {
				if (ex instanceof BatchUpdateException) {
					SQLException sqlEx = ((BatchUpdateException) ex).getNextException();

					throw new DatabaseOperationException("Error while inserting into the database", sqlEx);
				} else {
					throw new DatabaseOperationException("Error while inserting into the database", ex);
				}
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void insert(Database model, Collection dynaBeans) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			insert(connection, model, dynaBeans);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * Allows platforms to issue statements directly before rows are inserted into
	 * the specified table.
	 * 
	 * @param connection
	 *            The connection used for the insertion
	 * @param table
	 *            The table that the rows are inserted into
	 */
	protected void beforeInsert(Connection connection, Table table) throws SQLException {
	}

	/**
	 * Allows platforms to issue statements directly after rows have been inserted
	 * into the specified table.
	 * 
	 * @param connection
	 *            The connection used for the insertion
	 * @param table
	 *            The table that the rows have been inserted into
	 */
	protected void afterInsert(Connection connection, Table table) throws SQLException {
	}

	/**
	 * Creates the SQL for updating an object of the given type. If a concrete bean
	 * is given, then a concrete update statement is created, otherwise an update
	 * statement usable in a prepared statement is build.
	 * 
	 * @param model
	 *            The database model
	 * @param dynaClass
	 *            The type
	 * @param primaryKeys
	 *            The primary keys
	 * @param properties
	 *            The properties to write
	 * @param bean
	 *            Optionally the concrete bean to update
	 * @return The SQL required to update the instance
	 */
	protected String createUpdateSql(Database model, SqlDynaClass dynaClass, SqlDynaProperty[] primaryKeys,
			SqlDynaProperty[] properties, DynaBean bean) {
		Table table = model.findTable(dynaClass.getTableName());
		HashMap columnValues = toColumnValues(properties, bean);

		columnValues.putAll(toColumnValues(primaryKeys, bean));

		return _builder.getUpdateSql(table, columnValues, bean == null);
	}

	/**
	 * Creates the SQL for updating an object of the given type. If a concrete bean
	 * is given, then a concrete update statement is created, otherwise an update
	 * statement usable in a prepared statement is build.
	 * 
	 * @param model
	 *            The database model
	 * @param dynaClass
	 *            The type
	 * @param primaryKeys
	 *            The primary keys
	 * @param properties
	 *            The properties to write
	 * @param oldBean
	 *            Contains column values to identify the rows to update (i.e. for
	 *            the WHERE clause)
	 * @param newBean
	 *            Contains the new column values to write
	 * @return The SQL required to update the instance
	 */
	protected String createUpdateSql(Database model, SqlDynaClass dynaClass, SqlDynaProperty[] primaryKeys,
			SqlDynaProperty[] properties, DynaBean oldBean, DynaBean newBean) {
		Table table = model.findTable(dynaClass.getTableName());
		HashMap oldColumnValues = toColumnValues(primaryKeys, oldBean);
		HashMap newColumnValues = toColumnValues(properties, newBean);

		if (primaryKeys.length == 0) {
			_log.info("Cannot update instances of type " + dynaClass + " because it has no primary keys");
			return null;
		} else {
			return _builder.getUpdateSql(table, oldColumnValues, newColumnValues, newBean == null);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public String getUpdateSql(Database model, DynaBean dynaBean) {
		SqlDynaClass dynaClass = model.getDynaClassFor(dynaBean);
		SqlDynaProperty[] primaryKeys = dynaClass.getPrimaryKeyProperties();
		SqlDynaProperty[] nonPrimaryKeys = dynaClass.getNonPrimaryKeyProperties();

		if (primaryKeys.length == 0) {
			_log.info("Cannot update instances of type " + dynaClass + " because it has no primary keys");
			return null;
		} else {
			return createUpdateSql(model, dynaClass, primaryKeys, nonPrimaryKeys, dynaBean);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public String getUpdateSql(Database model, DynaBean oldDynaBean, DynaBean newDynaBean) {
		SqlDynaClass dynaClass = model.getDynaClassFor(oldDynaBean);
		SqlDynaProperty[] primaryKeys = dynaClass.getPrimaryKeyProperties();
		SqlDynaProperty[] nonPrimaryKeys = dynaClass.getNonPrimaryKeyProperties();

		if (primaryKeys.length == 0) {
			_log.info("Cannot update instances of type " + dynaClass + " because it has no primary keys");
			return null;
		} else {
			return createUpdateSql(model, dynaClass, primaryKeys, nonPrimaryKeys, oldDynaBean, newDynaBean);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void update(Connection connection, Database model, DynaBean dynaBean) throws DatabaseOperationException {
		SqlDynaClass dynaClass = model.getDynaClassFor(dynaBean);
		SqlDynaProperty[] primaryKeys = dynaClass.getPrimaryKeyProperties();

		if (primaryKeys.length == 0) {
			_log.info("Cannot update instances of type " + dynaClass + " because it has no primary keys");
			return;
		}

		SqlDynaProperty[] properties = dynaClass.getNonPrimaryKeyProperties();
		String sql = createUpdateSql(model, dynaClass, primaryKeys, properties, null);
		PreparedStatement statement = null;

		if (_log.isDebugEnabled()) {
			_log.debug("About to execute SQL: " + sql);
		}
		try {
			beforeUpdate(connection, dynaClass.getTable());

			statement = connection.prepareStatement(sql);

			int sqlIndex = 1;

			for (int idx = 0; idx < properties.length; idx++) {
				setObject(statement, sqlIndex++, dynaBean, properties[idx]);
			}
			for (int idx = 0; idx < primaryKeys.length; idx++) {
				setObject(statement, sqlIndex++, dynaBean, primaryKeys[idx]);
			}

			int count = statement.executeUpdate();

			afterUpdate(connection, dynaClass.getTable());

			if (count != 1) {
				_log.warn("Attempted to insert a single row " + dynaBean + " into table " + dynaClass.getTableName()
						+ " but changed " + count + " row(s)");
			}
		} catch (SQLException ex) {
			throw new DatabaseOperationException("Error while updating in the database", ex);
		} finally {
			closeStatement(statement);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void update(Database model, DynaBean dynaBean) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			update(connection, model, dynaBean);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void update(Connection connection, Database model, DynaBean oldDynaBean, DynaBean newDynaBean)
			throws DatabaseOperationException {
		SqlDynaClass dynaClass = model.getDynaClassFor(oldDynaBean);
		SqlDynaProperty[] primaryKeys = dynaClass.getPrimaryKeyProperties();

		if (!dynaClass.getTable().equals(model.getDynaClassFor(newDynaBean).getTable())) {
			throw new DatabaseOperationException("The old and new dyna beans need to be for the same table");
		}
		if (primaryKeys.length == 0) {
			_log.info("Cannot update instances of type " + dynaClass + " because it has no primary keys");
			return;
		}

		SqlDynaProperty[] properties = dynaClass.getSqlDynaProperties();
		String sql = createUpdateSql(model, dynaClass, primaryKeys, properties, null, null);
		PreparedStatement statement = null;

		if (_log.isDebugEnabled()) {
			_log.debug("About to execute SQL: " + sql);
		}
		try {
			beforeUpdate(connection, dynaClass.getTable());

			statement = connection.prepareStatement(sql);

			int sqlIndex = 1;

			for (int idx = 0; idx < properties.length; idx++) {
				setObject(statement, sqlIndex++, newDynaBean, properties[idx]);
			}
			for (int idx = 0; idx < primaryKeys.length; idx++) {
				setObject(statement, sqlIndex++, oldDynaBean, primaryKeys[idx]);
			}

			int count = statement.executeUpdate();

			afterUpdate(connection, dynaClass.getTable());

			if (count != 1) {
				_log.warn("Attempted to insert a single row " + newDynaBean + " into table " + dynaClass.getTableName()
						+ " but changed " + count + " row(s)");
			}
		} catch (SQLException ex) {
			throw new DatabaseOperationException("Error while updating in the database", ex);
		} finally {
			closeStatement(statement);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void update(Database model, DynaBean oldDynaBean, DynaBean newDynaBean) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			update(connection, model, oldDynaBean, newDynaBean);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * Allows platforms to issue statements directly before rows are updated in the
	 * specified table.
	 * 
	 * @param connection
	 *            The connection used for the update
	 * @param table
	 *            The table that the rows are updateed into
	 */
	protected void beforeUpdate(Connection connection, Table table) throws SQLException {
	}

	/**
	 * Allows platforms to issue statements directly after rows have been updated in
	 * the specified table.
	 * 
	 * @param connection
	 *            The connection used for the update
	 * @param table
	 *            The table that the rows have been updateed into
	 */
	protected void afterUpdate(Connection connection, Table table) throws SQLException {
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean exists(Database model, DynaBean dynaBean) {
		Connection connection = borrowConnection();

		try {
			return exists(connection, model, dynaBean);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean exists(Connection connection, Database model, DynaBean dynaBean) {
		SqlDynaClass dynaClass = model.getDynaClassFor(dynaBean);
		SqlDynaProperty[] primaryKeys = dynaClass.getPrimaryKeyProperties();

		if (primaryKeys.length == 0) {
			return false;
		}

		PreparedStatement stmt = null;

		try {
			StringBuffer sql = new StringBuffer();

			sql.append("SELECT * FROM ");
			sql.append(_builder.getDelimitedIdentifier(dynaClass.getTable().getName()));
			sql.append(" WHERE ");

			for (int idx = 0; idx < primaryKeys.length; idx++) {
				String key = primaryKeys[idx].getColumn().getName();

				if (idx > 0) {
					sql.append(" AND ");
				}
				sql.append(_builder.getDelimitedIdentifier(key));
				sql.append("=?");
			}

			stmt = connection.prepareStatement(sql.toString());

			for (int idx = 0; idx < primaryKeys.length; idx++) {
				setObject(stmt, idx + 1, dynaBean, primaryKeys[idx]);
			}

			ResultSet resultSet = stmt.executeQuery();

			return resultSet.next();
		} catch (SQLException ex) {
			throw new DatabaseOperationException("Error while reading from the database", ex);
		} finally {
			closeStatement(stmt);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void store(Database model, DynaBean dynaBean) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			store(connection, model, dynaBean);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void store(Connection connection, Database model, DynaBean dynaBean) throws DatabaseOperationException {
		if (exists(connection, model, dynaBean)) {
			update(connection, model, dynaBean);
		} else {
			insert(connection, model, dynaBean);
		}
	}

	/**
	 * Creates the SQL for deleting an object of the given type. If a concrete bean
	 * is given, then a concrete delete statement is created, otherwise a delete
	 * statement usable in a prepared statement is build.
	 * 
	 * @param model
	 *            The database model
	 * @param dynaClass
	 *            The type
	 * @param primaryKeys
	 *            The primary keys
	 * @param bean
	 *            Optionally the concrete bean to update
	 * @return The SQL required to delete the instance
	 */
	protected String createDeleteSql(Database model, SqlDynaClass dynaClass, SqlDynaProperty[] primaryKeys,
			DynaBean bean) {
		Table table = model.findTable(dynaClass.getTableName());
		HashMap pkValues = toColumnValues(primaryKeys, bean);

		return _builder.getDeleteSql(table, pkValues, bean == null);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getDeleteSql(Database model, DynaBean dynaBean) {
		SqlDynaClass dynaClass = model.getDynaClassFor(dynaBean);
		SqlDynaProperty[] primaryKeys = dynaClass.getPrimaryKeyProperties();

		if (primaryKeys.length == 0) {
			_log.warn("Cannot delete instances of type " + dynaClass + " because it has no primary keys");
			return null;
		} else {
			return createDeleteSql(model, dynaClass, primaryKeys, dynaBean);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void delete(Database model, DynaBean dynaBean) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			delete(connection, model, dynaBean);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void delete(Connection connection, Database model, DynaBean dynaBean) throws DatabaseOperationException {
		PreparedStatement statement = null;

		try {
			SqlDynaClass dynaClass = model.getDynaClassFor(dynaBean);
			SqlDynaProperty[] primaryKeys = dynaClass.getPrimaryKeyProperties();

			if (primaryKeys.length == 0) {
				_log.warn("Cannot delete instances of type " + dynaClass + " because it has no primary keys");
				return;
			}

			String sql = createDeleteSql(model, dynaClass, primaryKeys, null);

			if (_log.isDebugEnabled()) {
				_log.debug("About to execute SQL " + sql);
			}

			statement = connection.prepareStatement(sql);

			for (int idx = 0; idx < primaryKeys.length; idx++) {
				setObject(statement, idx + 1, dynaBean, primaryKeys[idx]);
			}

			int count = statement.executeUpdate();

			if (count != 1) {
				_log.warn("Attempted to delete a single row " + dynaBean + " in table " + dynaClass.getTableName()
						+ " but changed " + count + " row(s).");
			}
		} catch (SQLException ex) {
			throw new DatabaseOperationException("Error while deleting from the database", ex);
		} finally {
			closeStatement(statement);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public Database readModelFromDatabase(String name) throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			return readModelFromDatabase(connection, name);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public Database readModelFromDatabase(Connection connection, String name) throws DatabaseOperationException {
		try {
			Database model = getModelReader().getDatabase(connection, name);

			postprocessModelFromDatabase(model);
			return model;
		} catch (SQLException ex) {
			throw new DatabaseOperationException(ex);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public Database readModelFromDatabase(String name, String catalog, String schema, String[] tableTypes)
			throws DatabaseOperationException {
		Connection connection = borrowConnection();

		try {
			return readModelFromDatabase(connection, name, catalog, schema, tableTypes);
		} finally {
			returnConnection(connection);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public Database readModelFromDatabase(Connection connection, String name, String catalog, String schema,
			String[] tableTypes) throws DatabaseOperationException {
		try {
			JdbcModelReader reader = getModelReader();
			Database model = reader.getDatabase(connection, name, catalog, schema, tableTypes);

			postprocessModelFromDatabase(model);
			if ((model.getName() == null) || (model.getName().length() == 0)) {
				model.setName(MODEL_DEFAULT_NAME);
			}
			return model;
		} catch (SQLException ex) {
			throw new DatabaseOperationException(ex);
		}
	}

	/**
	 * Allows the platform to postprocess the model just read from the database.
	 * 
	 * @param model
	 *            The model
	 */
	protected void postprocessModelFromDatabase(Database model) {
		// Default values for CHAR/VARCHAR/LONGVARCHAR columns have quotation marks
		// around them which we'll remove now
		for (int tableIdx = 0; tableIdx < model.getTableCount(); tableIdx++) {
			Table table = model.getTable(tableIdx);

			for (int columnIdx = 0; columnIdx < table.getColumnCount(); columnIdx++) {
				Column column = table.getColumn(columnIdx);

				if (TypeMap.isTextType(column.getTypeCode()) || TypeMap.isDateTimeType(column.getTypeCode())) {
					String defaultValue = column.getDefaultValue();

					if ((defaultValue != null) && (defaultValue.length() >= 2) && defaultValue.startsWith("'")
							&& defaultValue.endsWith("'")) {
						defaultValue = defaultValue.substring(1, defaultValue.length() - 1);
						column.setDefaultValue(defaultValue);
					}
				}
			}
		}
	}

	/**
	 * Derives the column values for the given dyna properties from the dyna bean.
	 * 
	 * @param properties
	 *            The properties
	 * @param bean
	 *            The bean
	 * @return The values indexed by the column names
	 */
	protected HashMap toColumnValues(SqlDynaProperty[] properties, DynaBean bean) {
		HashMap result = new HashMap<>();

		for (int idx = 0; idx < properties.length; idx++) {
			result.put(properties[idx].getName(), bean == null ? null : bean.get(properties[idx].getName()));
		}
		return result;
	}

	/**
	 * Sets a parameter of the prepared statement based on the type of the column of
	 * the property.
	 * 
	 * @param statement
	 *            The statement
	 * @param sqlIndex
	 *            The index of the parameter to set in the statement
	 * @param dynaBean
	 *            The bean of which to take the value
	 * @param property
	 *            The property of the bean, which also defines the corresponding
	 *            column
	 */
	protected void setObject(PreparedStatement statement, int sqlIndex, DynaBean dynaBean, SqlDynaProperty property)
			throws SQLException {
		int typeCode = property.getColumn().getTypeCode();
		Object value = dynaBean.get(property.getName());

		setStatementParameterValue(statement, sqlIndex, typeCode, value);
	}

	/**
	 * This is the core method to set the parameter of a prepared statement to a
	 * given value. The primary purpose of this method is to call the appropriate
	 * method on the statement, and to give database-specific implementations the
	 * ability to change this behavior.
	 * 
	 * @param statement
	 *            The statement
	 * @param sqlIndex
	 *            The parameter index
	 * @param typeCode
	 *            The JDBC type code
	 * @param value
	 *            The value
	 * @throws SQLException
	 *             If an error occurred while setting the parameter value
	 */
	protected void setStatementParameterValue(PreparedStatement statement, int sqlIndex, int typeCode, Object value)
			throws SQLException {
		if (value == null) {
			statement.setNull(sqlIndex, typeCode);
		} else if (value instanceof String) {
			statement.setString(sqlIndex, (String) value);
		} else if (value instanceof byte[]) {
			statement.setBytes(sqlIndex, (byte[]) value);
		} else if (value instanceof Boolean) {
			statement.setBoolean(sqlIndex, ((Boolean) value).booleanValue());
		} else if (value instanceof Byte) {
			statement.setByte(sqlIndex, ((Byte) value).byteValue());
		} else if (value instanceof Short) {
			statement.setShort(sqlIndex, ((Short) value).shortValue());
		} else if (value instanceof Integer) {
			statement.setInt(sqlIndex, ((Integer) value).intValue());
		} else if (value instanceof Long) {
			statement.setLong(sqlIndex, ((Long) value).longValue());
		} else if (value instanceof BigDecimal) {
			// setObject assumes a scale of 0, so we rather use the typed setter
			statement.setBigDecimal(sqlIndex, (BigDecimal) value);
		} else if (value instanceof Float) {
			statement.setFloat(sqlIndex, ((Float) value).floatValue());
		} else if (value instanceof Double) {
			statement.setDouble(sqlIndex, ((Double) value).doubleValue());
		} else {
			statement.setObject(sqlIndex, value, typeCode);
		}
	}

	/**
	 * Helper method esp. for the {@link ModelBasedResultSetIterator} class that
	 * retrieves the value for a column from the given result set. If a table was
	 * specified, and it contains the column, then the jdbc type defined for the
	 * column is used for extracting the value, otherwise the object directly
	 * retrieved from the result set is returned.
* The method is defined here rather than in the * {@link ModelBasedResultSetIterator} class so that concrete platforms can * modify its behavior. * * @param resultSet * The result set * @param columnName * The name of the column * @param table * The table * @return The value */ protected Object getObjectFromResultSet(ResultSet resultSet, String columnName, Table table) throws SQLException { Column column = (table == null ? null : table.findColumn(columnName, isDelimitedIdentifierModeOn())); Object value = null; if (column != null) { int originalJdbcType = column.getTypeCode(); int targetJdbcType = getPlatformInfo().getTargetJdbcType(originalJdbcType); int jdbcType = originalJdbcType; // in general we're trying to retrieve the value using the original type // but sometimes we also need the target type: if ((originalJdbcType == Types.BLOB) && (targetJdbcType != Types.BLOB)) { // we should not use the Blob interface if the database doesn't map to this type jdbcType = targetJdbcType; } if ((originalJdbcType == Types.CLOB) && (targetJdbcType != Types.CLOB)) { // we should not use the Clob interface if the database doesn't map to this type jdbcType = targetJdbcType; } value = extractColumnValue(resultSet, columnName, 0, jdbcType); } else { value = resultSet.getObject(columnName); } return resultSet.wasNull() ? null : value; } /** * Helper method for retrieving the value for a column from the given result set * using the type code of the column. * * @param resultSet * The result set * @param column * The column * @param idx * The value's index in the result set (starting from 1) * @return The value */ protected Object getObjectFromResultSet(ResultSet resultSet, Column column, int idx) throws SQLException { int originalJdbcType = column.getTypeCode(); int targetJdbcType = getPlatformInfo().getTargetJdbcType(originalJdbcType); int jdbcType = originalJdbcType; Object value = null; // in general we're trying to retrieve the value using the original type // but sometimes we also need the target type: if ((originalJdbcType == Types.BLOB) && (targetJdbcType != Types.BLOB)) { // we should not use the Blob interface if the database doesn't map to this type jdbcType = targetJdbcType; } if ((originalJdbcType == Types.CLOB) && (targetJdbcType != Types.CLOB)) { // we should not use the Clob interface if the database doesn't map to this type jdbcType = targetJdbcType; } value = extractColumnValue(resultSet, null, idx, jdbcType); return resultSet.wasNull() ? null : value; } /** * This is the core method to retrieve a value for a column from a result set. * Its primary purpose is to call the appropriate method on the result set, and * to provide an extension point where database-specific implementations can * change this behavior. * * @param resultSet * The result set to extract the value from * @param columnName * The name of the column; can be null in which case the * columnIdx will be used instead * @param columnIdx * The index of the column's value in the result set; is only used if * columnName is null * @param jdbcType * The jdbc type to extract * @return The value * @throws SQLException * If an error occurred while accessing the result set */ protected Object extractColumnValue(ResultSet resultSet, String columnName, int columnIdx, int jdbcType) throws SQLException { boolean useIdx = (columnName == null); Object value; switch (jdbcType) { case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: value = useIdx ? resultSet.getString(columnIdx) : resultSet.getString(columnName); break; case Types.NUMERIC: case Types.DECIMAL: value = useIdx ? resultSet.getBigDecimal(columnIdx) : resultSet.getBigDecimal(columnName); break; case Types.BIT: case Types.BOOLEAN: value = new Boolean(useIdx ? resultSet.getBoolean(columnIdx) : resultSet.getBoolean(columnName)); break; case Types.TINYINT: case Types.SMALLINT: case Types.INTEGER: value = new Integer(useIdx ? resultSet.getInt(columnIdx) : resultSet.getInt(columnName)); break; case Types.BIGINT: value = new Long(useIdx ? resultSet.getLong(columnIdx) : resultSet.getLong(columnName)); break; case Types.REAL: value = new Float(useIdx ? resultSet.getFloat(columnIdx) : resultSet.getFloat(columnName)); break; case Types.FLOAT: case Types.DOUBLE: value = new Double(useIdx ? resultSet.getDouble(columnIdx) : resultSet.getDouble(columnName)); break; case Types.BINARY: case Types.VARBINARY: case Types.LONGVARBINARY: value = useIdx ? resultSet.getBytes(columnIdx) : resultSet.getBytes(columnName); break; case Types.DATE: value = useIdx ? resultSet.getDate(columnIdx) : resultSet.getDate(columnName); break; case Types.TIME: value = useIdx ? resultSet.getTime(columnIdx) : resultSet.getTime(columnName); break; case Types.TIMESTAMP: value = useIdx ? resultSet.getTimestamp(columnIdx) : resultSet.getTimestamp(columnName); break; case Types.CLOB: Clob clob = useIdx ? resultSet.getClob(columnIdx) : resultSet.getClob(columnName); if (clob == null) { value = null; } else { long length = clob.length(); if (length > Integer.MAX_VALUE) { value = clob; } else if (length == 0) { // the javadoc is not clear about whether Clob.getSubString // can be used with a substring length of 0 // thus we do the safe thing and handle it ourselves value = ""; } else { value = clob.getSubString(1l, (int) length); } } break; case Types.BLOB: Blob blob = useIdx ? resultSet.getBlob(columnIdx) : resultSet.getBlob(columnName); if (blob == null) { value = null; } else { long length = blob.length(); if (length > Integer.MAX_VALUE) { value = blob; } else if (length == 0) { // the javadoc is not clear about whether Blob.getBytes // can be used with for 0 bytes to be copied // thus we do the safe thing and handle it ourselves value = new byte[0]; } else { value = blob.getBytes(1l, (int) length); } } break; case Types.ARRAY: value = useIdx ? resultSet.getArray(columnIdx) : resultSet.getArray(columnName); break; case Types.REF: value = useIdx ? resultSet.getRef(columnIdx) : resultSet.getRef(columnName); break; default: value = useIdx ? resultSet.getObject(columnIdx) : resultSet.getObject(columnName); break; } return resultSet.wasNull() ? null : value; } /** * Creates an iterator over the given result set. * * @param model * The database model * @param resultSet * The result set to iterate over * @param queryHints * The tables that were queried in the query that produced the given * result set (optional) * @return The iterator */ protected ModelBasedResultSetIterator createResultSetIterator(Database model, ResultSet resultSet, Table[] queryHints) { return new ModelBasedResultSetIterator(this, model, resultSet, queryHints, true); } }