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

org.jumpmind.symmetric.ddl.platform.SqlBuilder Maven / Gradle / Ivy

Go to download

This library is a fork of Apache DdlUtils. DdlUtils is a small, easy-to-use component for working with Database Definition (DDL) files.

There is a newer version: 2.5.13
Show newest version
package org.jumpmind.symmetric.ddl.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 java.io.IOException;
import java.io.Writer;
import java.rmi.server.UID;
import java.sql.Types;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections.Closure;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.map.ListOrderedMap;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jumpmind.symmetric.ddl.DdlUtilsException;
import org.jumpmind.symmetric.ddl.Platform;
import org.jumpmind.symmetric.ddl.PlatformInfo;
import org.jumpmind.symmetric.ddl.alteration.AddColumnChange;
import org.jumpmind.symmetric.ddl.alteration.AddForeignKeyChange;
import org.jumpmind.symmetric.ddl.alteration.AddIndexChange;
import org.jumpmind.symmetric.ddl.alteration.AddPrimaryKeyChange;
import org.jumpmind.symmetric.ddl.alteration.AddTableChange;
import org.jumpmind.symmetric.ddl.alteration.ColumnAutoIncrementChange;
import org.jumpmind.symmetric.ddl.alteration.ColumnDataTypeChange;
import org.jumpmind.symmetric.ddl.alteration.ColumnDefaultValueChange;
import org.jumpmind.symmetric.ddl.alteration.ColumnRequiredChange;
import org.jumpmind.symmetric.ddl.alteration.ColumnSizeChange;
import org.jumpmind.symmetric.ddl.alteration.ModelChange;
import org.jumpmind.symmetric.ddl.alteration.ModelComparator;
import org.jumpmind.symmetric.ddl.alteration.PrimaryKeyChange;
import org.jumpmind.symmetric.ddl.alteration.RemoveColumnChange;
import org.jumpmind.symmetric.ddl.alteration.RemoveForeignKeyChange;
import org.jumpmind.symmetric.ddl.alteration.RemoveIndexChange;
import org.jumpmind.symmetric.ddl.alteration.RemovePrimaryKeyChange;
import org.jumpmind.symmetric.ddl.alteration.RemoveTableChange;
import org.jumpmind.symmetric.ddl.alteration.TableChange;
import org.jumpmind.symmetric.ddl.model.Column;
import org.jumpmind.symmetric.ddl.model.Database;
import org.jumpmind.symmetric.ddl.model.ForeignKey;
import org.jumpmind.symmetric.ddl.model.Index;
import org.jumpmind.symmetric.ddl.model.IndexColumn;
import org.jumpmind.symmetric.ddl.model.ModelException;
import org.jumpmind.symmetric.ddl.model.Reference;
import org.jumpmind.symmetric.ddl.model.Table;
import org.jumpmind.symmetric.ddl.model.TypeMap;
import org.jumpmind.symmetric.ddl.util.CallbackClosure;
import org.jumpmind.symmetric.ddl.util.MultiInstanceofPredicate;

/**
 * This class is a collection of Strategy methods for creating the DDL required to create and drop 
 * databases and tables.
 * 
 * It is hoped that just a single implementation of this class, for each database should make creating DDL
 * for each physical database fairly straightforward.
 * 
 * An implementation of this class can always delegate down to some templating technology such as Velocity if
 * it requires. Though often that can be quite complex when attempting to reuse code across many databases.
 * Hopefully only a small amount code needs to be changed on a per database basis.
 * 
 * @version $Revision: 518498 $
 */
public abstract class SqlBuilder
{
    /** The line separator for in between sql commands. */
    private static final String LINE_SEPARATOR = System.getProperty("line.separator", "\n");
    /** The placeholder for the size value in the native type spec. */
    protected static final String SIZE_PLACEHOLDER = "{0}";

    /** The Log to which logging calls will be made. */
    protected final Log _log = LogFactory.getLog(SqlBuilder.class);
    
    /** The platform that this builder belongs to. */
    private Platform _platform;
    /** The current Writer used to output the SQL to. */
    private Writer _writer;
    /** The indentation used to indent commands. */
    private String _indent = "    ";
    /** An optional locale specification for number and date formatting. */
    private String _valueLocale;
    /** The date formatter. */
    private DateFormat _valueDateFormat;
    /** The date time formatter. */
    private DateFormat _valueTimeFormat;
    /** The number formatter. */
    private NumberFormat _valueNumberFormat;
    /** Helper object for dealing with default values. */
    private DefaultValueHelper _defaultValueHelper = new DefaultValueHelper();
    /** The character sequences that need escaping. */
    private Map _charSequencesToEscape = new ListOrderedMap();

    //
    // Configuration
    //                

    /**
     * Creates a new sql builder.
     * 
     * @param platform The plaftform this builder belongs to
     */
    public SqlBuilder(Platform platform)
    {
        _platform = platform;
    }

    /**
     * Returns the platform object.
     * 
     * @return The platform
     */
    public Platform getPlatform()
    {
        return _platform;
    }

    /**
     * Returns the platform info object.
     * 
     * @return The info object
     */
    public PlatformInfo getPlatformInfo()
    {
        return _platform.getPlatformInfo();
    }

    /**
     * Returns the writer that the DDL is printed to.
     * 
     * @return The writer
     */
    public Writer getWriter()
    {
        return _writer;
    }

    /**
     * Sets the writer for printing the DDL to.
     * 
     * @param writer The writer
     */
    public void setWriter(Writer writer)
    {
        _writer = writer;
    }

    /**
     * Returns the default value helper.
     *
     * @return The default value helper
     */
    public DefaultValueHelper getDefaultValueHelper()
    {
        return _defaultValueHelper;
    }

    /** 
     * Returns the string used to indent the SQL.
     * 
     * @return The indentation string
     */
    public String getIndent()
    {
        return _indent;
    }

    /**
     * Sets the string used to indent the SQL.
     * 
     * @param indent The indentation string
     */
    public void setIndent(String indent)
    {
        _indent = indent;
    }

    /**
     * Returns the locale that is used for number and date formatting
     * (when printing default values and in generates insert/update/delete
     * statements).
     * 
     * @return The locale or null if default formatting is used
     */
    public String getValueLocale()
    {
        return _valueLocale;
    }

    /**
     * Sets the locale that is used for number and date formatting
     * (when printing default values and in generates insert/update/delete
     * statements).
     *
     * @param localeStr The new locale or null if default formatting
     *                  should be used; Format is "language[_country[_variant]]"
     */
    public void setValueLocale(String localeStr)
    {
        if (localeStr != null)
        {
            int    sepPos   = localeStr.indexOf('_');
            String language = null;
            String country  = null;
            String variant  = null;

            if (sepPos > 0)
            {
                language = localeStr.substring(0, sepPos);
                country  = localeStr.substring(sepPos + 1);
                sepPos   = country.indexOf('_');
                if (sepPos > 0)
                {
                    variant = country.substring(sepPos + 1);
                    country = country.substring(0, sepPos);
                }
            }
            else
            {
                language = localeStr;
            }
            if (language != null)
            {
                Locale locale = null;
    
                if (variant != null)
                {
                    locale = new Locale(language, country, variant);
                }
                else if (country != null)
                {
                    locale = new Locale(language, country);
                }
                else
                {
                    locale = new Locale(language);
                }

                _valueLocale = localeStr;
                setValueDateFormat(DateFormat.getDateInstance(DateFormat.SHORT, locale));
                setValueTimeFormat(DateFormat.getTimeInstance(DateFormat.SHORT, locale));
                setValueNumberFormat(NumberFormat.getNumberInstance(locale));
                return;
            }
        }
        _valueLocale = null;
        setValueDateFormat(null);
        setValueTimeFormat(null);
        setValueNumberFormat(null);
    }

    /**
     * Returns the format object for formatting dates in the specified locale.
     * 
     * @return The date format object or null if no locale is set
     */
    protected DateFormat getValueDateFormat()
    {
        return _valueDateFormat;
    }
    
    /**
     * Sets the format object for formatting dates in the specified locale.
     * 
     * @param format The date format object
     */
    protected void setValueDateFormat(DateFormat format)
    {
        _valueDateFormat = format;
    }

    /**
     * Returns the format object for formatting times in the specified locale.
     * 
     * @return The time format object or null if no locale is set
     */
    protected DateFormat getValueTimeFormat()
    {
        return _valueTimeFormat;
    }

    /**
     * Sets the date format object for formatting times in the specified locale.
     * 
     * @param format The time format object
     */
    protected void setValueTimeFormat(DateFormat format)
    {
        _valueTimeFormat = format;
    }

    /**
     * Returns the format object for formatting numbers in the specified locale.
     * 
     * @return The number format object or null if no locale is set
     */
    protected NumberFormat getValueNumberFormat()
    {
        return _valueNumberFormat;
    }

    /**
     * Returns a new date format object for formatting numbers in the specified locale.
     * Platforms can override this if necessary.
     * 
     * @param format The number format object
     */
    protected void setValueNumberFormat(NumberFormat format)
    {
        _valueNumberFormat = format;
    }
    
    /**
     * Adds a char sequence that needs escaping, and its escaped version.
     * 
     * @param charSequence   The char sequence
     * @param escapedVersion The escaped version
     */
    protected void addEscapedCharSequence(String charSequence, String escapedVersion)
    {
        _charSequencesToEscape.put(charSequence, escapedVersion);
    }

    /**
     * Returns the maximum number of characters that a table name can have.
     * This method is intended to give platform specific builder implementations
     * more control over the maximum length.
     * 
     * @return The number of characters, or -1 if not limited
     */
    public int getMaxTableNameLength()
    {
        return getPlatformInfo().getMaxTableNameLength();
    }

    /**
     * Returns the maximum number of characters that a column name can have.
     * This method is intended to give platform specific builder implementations
     * more control over the maximum length.
     * 
     * @return The number of characters, or -1 if not limited
     */
    public int getMaxColumnNameLength()
    {
        return getPlatformInfo().getMaxColumnNameLength();
    }

    /**
     * Returns the maximum number of characters that a constraint name can have.
     * This method is intended to give platform specific builder implementations
     * more control over the maximum length.
     * 
     * @return The number of characters, or -1 if not limited
     */
    public int getMaxConstraintNameLength()
    {
        return getPlatformInfo().getMaxConstraintNameLength();
    }

    /**
     * Returns the maximum number of characters that a foreign key name can have.
     * This method is intended to give platform specific builder implementations
     * more control over the maximum length.
     * 
     * @return The number of characters, or -1 if not limited
     */
    public int getMaxForeignKeyNameLength()
    {
        return getPlatformInfo().getMaxForeignKeyNameLength();
    }

    //
    // public interface
    //

    /**
     * Outputs the DDL required to drop and (re)create all tables in the database model.
     * 
     * @param database The database model 
     */
    public void createTables(Database database) throws IOException
    {
        createTables(database, null, true);
    }

    /**
     * Outputs the DDL required to drop (if requested) and (re)create all tables in the database model.
     * 
     * @param database   The database
     * @param dropTables Whether to drop tables before creating them
     */
    public void createTables(Database database, boolean dropTables) throws IOException
    {
        createTables(database, null, dropTables);
    }

    /**
     * Outputs the DDL required to drop (if requested) and (re)create all tables in the database model.
     * 
     * @param database   The database
     * @param params     The parameters used in the creation
     * @param dropTables Whether to drop tables before creating them
     */
    public void createTables(Database database, CreationParameters params, boolean dropTables) throws IOException
    {
        if (dropTables)
        {
            dropTables(database);
        }

        for (int idx = 0; idx < database.getTableCount(); idx++)
        {
            Table table = database.getTable(idx);

            writeTableComment(table);
            createTable(database,
                        table,
                        params == null ? null : params.getParametersFor(table));
        }

        // we're writing the external foreignkeys last to ensure that all referenced tables are already defined
        createExternalForeignKeys(database);
    }

    /**
     * Generates the DDL to modify an existing database so the schema matches
     * the specified database schema by using drops, modifications and additions.
     * Database-specific implementations can change aspect of this algorithm by
     * redefining the individual methods that compromise it.
     *
     * @param currentModel  The current database schema
     * @param desiredModel  The desired database schema
     * @param params        The parameters used in the creation of new tables. Note that for existing
     *                      tables, the parameters won't be applied
     */
    public void alterDatabase(Database currentModel, Database desiredModel, CreationParameters params) throws IOException
    {
        ModelComparator comparator = new ModelComparator(getPlatformInfo(),
                                                         getPlatform().isDelimitedIdentifierModeOn());
        List            changes    = comparator.compare(currentModel, desiredModel);

        processChanges(currentModel, desiredModel, changes, params);
    }

    public boolean isAlterDatabase(Database currentModel, Database desiredModel, CreationParameters params) throws IOException
    {
        ModelComparator comparator = new ModelComparator(getPlatformInfo(),
                                                         getPlatform().isDelimitedIdentifierModeOn());
        List            changes    = comparator.compare(currentModel, desiredModel);

        return changes.size() > 0;
    }

    /**
     * Calls the given closure for all changes that are of one of the given types, and
     * then removes them from the changes collection.
     * 
     * @param changes     The changes
     * @param changeTypes The types to search for
     * @param closure     The closure to invoke
     */
    protected void applyForSelectedChanges(Collection changes, Class[] changeTypes, final Closure closure)
    {
        final Predicate predicate = new MultiInstanceofPredicate(changeTypes);

        // basically we filter the changes for all objects where the above predicate
        // returns true, and for these filtered objects we invoke the given closure
        CollectionUtils.filter(changes,
                               new Predicate()
                               {
                                   public boolean evaluate(Object obj)
                                   {
                                       if (predicate.evaluate(obj))
                                       {
                                           closure.execute(obj);
                                           return false;
                                       }
                                       else
                                       {
                                           return true;
                                       }
                                   }
                               });
    }
    
    /**
     * Processes the changes. The default argument performs several passes:
     * 
    *
  1. {@link org.jumpmind.symmetric.ddl.alteration.RemoveForeignKeyChange} and * {@link org.jumpmind.symmetric.ddl.alteration.RemoveIndexChange} come first * to allow for e.g. subsequent primary key changes or column removal.
  2. *
  3. {@link org.jumpmind.symmetric.ddl.alteration.RemoveTableChange} * comes after the removal of foreign keys and indices.
  4. *
  5. These are all handled together:
    * {@link org.jumpmind.symmetric.ddl.alteration.RemovePrimaryKeyChange}
    * {@link org.jumpmind.symmetric.ddl.alteration.AddPrimaryKeyChange}
    * {@link org.jumpmind.symmetric.ddl.alteration.PrimaryKeyChange}
    * {@link org.jumpmind.symmetric.ddl.alteration.RemoveColumnChange}
    * {@link org.jumpmind.symmetric.ddl.alteration.AddColumnChange}
    * {@link org.jumpmind.symmetric.ddl.alteration.ColumnAutoIncrementChange}
    * {@link org.jumpmind.symmetric.ddl.alteration.ColumnDefaultValueChange}
    * {@link org.jumpmind.symmetric.ddl.alteration.ColumnRequiredChange}
    * {@link org.jumpmind.symmetric.ddl.alteration.ColumnDataTypeChange}
    * {@link org.jumpmind.symmetric.ddl.alteration.ColumnSizeChange}
    * The reason for this is that the default algorithm rebuilds the table for these * changes and thus their order is irrelevant.
  6. *
  7. {@link org.jumpmind.symmetric.ddl.alteration.AddTableChange}
    * needs to come after the table removal (so that tables of the same name are * removed) and before the addition of foreign keys etc.
  8. *
  9. {@link org.jumpmind.symmetric.ddl.alteration.AddForeignKeyChange} and * {@link org.jumpmind.symmetric.ddl.alteration.AddIndexChange} come last * after table/column/primary key additions or changes.
  10. *
* * @param currentModel The current database schema * @param desiredModel The desired database schema * @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 */ protected void processChanges(Database currentModel, Database desiredModel, List changes, CreationParameters params) throws IOException { CallbackClosure callbackClosure = new CallbackClosure(this, "processChange", new Class[] { Database.class, Database.class, CreationParameters.class, null }, new Object[] { currentModel, desiredModel, params, null }); // 1st pass: removing external constraints and indices applyForSelectedChanges(changes, new Class[] { RemoveForeignKeyChange.class, RemoveIndexChange.class }, callbackClosure); // 2nd pass: removing tables applyForSelectedChanges(changes, new Class[] { RemoveTableChange.class }, callbackClosure); // 3rd pass: changing the structure of tables Predicate predicate = new MultiInstanceofPredicate(new Class[] { RemovePrimaryKeyChange.class, AddPrimaryKeyChange.class, PrimaryKeyChange.class, RemoveColumnChange.class, AddColumnChange.class, ColumnAutoIncrementChange.class, ColumnDefaultValueChange.class, ColumnRequiredChange.class, ColumnDataTypeChange.class, ColumnSizeChange.class }); processTableStructureChanges(currentModel, desiredModel, params, CollectionUtils.select(changes, predicate)); // 4th pass: adding tables applyForSelectedChanges(changes, new Class[] { AddTableChange.class }, callbackClosure); // 5th pass: adding external constraints and indices applyForSelectedChanges(changes, new Class[] { AddForeignKeyChange.class, AddIndexChange.class }, callbackClosure); } /** * This is a fall-through callback which generates a warning because a specific * change type wasn't handled. * * @param currentModel The current database schema * @param desiredModel The desired 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 */ protected void processChange(Database currentModel, Database desiredModel, CreationParameters params, ModelChange change) throws IOException { _log.warn("Change of type " + change.getClass() + " was not handled"); } /** * Processes the change representing the removal of a foreign key. * * @param currentModel The current database schema * @param desiredModel The desired 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 */ protected void processChange(Database currentModel, Database desiredModel, CreationParameters params, RemoveForeignKeyChange change) throws IOException { writeExternalForeignKeyDropStmt(change.getChangedTable(), change.getForeignKey()); change.apply(currentModel, getPlatform().isDelimitedIdentifierModeOn()); } /** * Processes the change representing the removal of an index. * * @param currentModel The current database schema * @param desiredModel The desired 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 */ protected void processChange(Database currentModel, Database desiredModel, CreationParameters params, RemoveIndexChange change) throws IOException { writeExternalIndexDropStmt(change.getChangedTable(), change.getIndex()); change.apply(currentModel, getPlatform().isDelimitedIdentifierModeOn()); } /** * Processes the change representing the removal of a table. * * @param currentModel The current database schema * @param desiredModel The desired 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 */ protected void processChange(Database currentModel, Database desiredModel, CreationParameters params, RemoveTableChange change) throws IOException { dropTable(change.getChangedTable()); change.apply(currentModel, getPlatform().isDelimitedIdentifierModeOn()); } /** * Processes the change representing the addition of a table. * * @param currentModel The current database schema * @param desiredModel The desired 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 */ protected void processChange(Database currentModel, Database desiredModel, CreationParameters params, AddTableChange change) throws IOException { createTable(desiredModel, change.getNewTable(), params == null ? null : params.getParametersFor(change.getNewTable())); change.apply(currentModel, getPlatform().isDelimitedIdentifierModeOn()); } /** * Processes the change representing the addition of a foreign key. * * @param currentModel The current database schema * @param desiredModel The desired 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 */ protected void processChange(Database currentModel, Database desiredModel, CreationParameters params, AddForeignKeyChange change) throws IOException { writeExternalForeignKeyCreateStmt(desiredModel, change.getChangedTable(), change.getNewForeignKey()); change.apply(currentModel, getPlatform().isDelimitedIdentifierModeOn()); } /** * Processes the change representing the addition of an index. * * @param currentModel The current database schema * @param desiredModel The desired 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 */ protected void processChange(Database currentModel, Database desiredModel, CreationParameters params, AddIndexChange change) throws IOException { writeExternalIndexCreateStmt(change.getChangedTable(), change.getNewIndex()); change.apply(currentModel, getPlatform().isDelimitedIdentifierModeOn()); } /** * Processes the changes to the structure of tables. * * @param currentModel The current database schema * @param desiredModel The desired 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 changes The change objects */ protected void processTableStructureChanges(Database currentModel, Database desiredModel, CreationParameters params, Collection changes) throws IOException { ListOrderedMap changesPerTable = new ListOrderedMap(); ListOrderedMap unchangedFKs = new ListOrderedMap(); boolean caseSensitive = getPlatform().isDelimitedIdentifierModeOn(); // we first sort the changes for the tables // however since the changes might contain source or target tables // we use the names rather than the table objects for (Iterator changeIt = changes.iterator(); changeIt.hasNext();) { TableChange change = changeIt.next(); String name = change.getChangedTable().getName(); if (!caseSensitive) { name = name.toUpperCase(); } List changesForTable = (List)changesPerTable.get(name); if (changesForTable == null) { changesForTable = new ArrayList(); changesPerTable.put(name, changesForTable); unchangedFKs.put(name, getUnchangedForeignKeys(currentModel, desiredModel, name)); } changesForTable.add(change); } // we also need to drop the foreign keys of the unchanged tables referencing the changed tables addRelevantFKsFromUnchangedTables(currentModel, desiredModel, changesPerTable.keySet(), unchangedFKs); // we're dropping the unchanged foreign keys for (Iterator tableFKIt = unchangedFKs.entrySet().iterator(); tableFKIt.hasNext();) { Map.Entry entry = (Map.Entry)tableFKIt.next(); Table targetTable = desiredModel.findTable((String)entry.getKey(), caseSensitive); for (Iterator fkIt = ((List)entry.getValue()).iterator(); fkIt.hasNext();) { writeExternalForeignKeyDropStmt(targetTable, (ForeignKey)fkIt.next()); } } // We're using a copy of the current model so that the table structure changes can // modify it Database copyOfCurrentModel = null; try { copyOfCurrentModel = (Database)currentModel.clone(); } catch (CloneNotSupportedException ex) { throw new DdlUtilsException(ex); } for (Iterator tableChangeIt = changesPerTable.entrySet().iterator(); tableChangeIt.hasNext();) { Map.Entry entry = (Map.Entry)tableChangeIt.next(); Table targetTable = desiredModel.findTable((String)entry.getKey(), caseSensitive); processTableStructureChanges(copyOfCurrentModel, desiredModel, (String)entry.getKey(), params == null ? null : params.getParametersFor(targetTable), (List)entry.getValue()); } // and finally we're re-creating the unchanged foreign keys for (Iterator tableFKIt = unchangedFKs.entrySet().iterator(); tableFKIt.hasNext();) { Map.Entry entry = (Map.Entry)tableFKIt.next(); Table targetTable = desiredModel.findTable((String)entry.getKey(), caseSensitive); for (Iterator fkIt = ((List)entry.getValue()).iterator(); fkIt.hasNext();) { writeExternalForeignKeyCreateStmt(desiredModel, targetTable, (ForeignKey)fkIt.next()); } } } /** * Determines the unchanged foreign keys of the indicated table. * * @param currentModel The current model * @param desiredModel The desired model * @param tableName The name of the table * @return The list of unchanged foreign keys */ private List getUnchangedForeignKeys(Database currentModel, Database desiredModel, String tableName) { ArrayList unchangedFKs = new ArrayList(); boolean caseSensitive = getPlatform().isDelimitedIdentifierModeOn(); Table sourceTable = currentModel.findTable(tableName, caseSensitive); Table targetTable = desiredModel.findTable(tableName, caseSensitive); for (int idx = 0; idx < targetTable.getForeignKeyCount(); idx++) { ForeignKey targetFK = targetTable.getForeignKey(idx); ForeignKey sourceFK = sourceTable.findForeignKey(targetFK, caseSensitive); if (sourceFK != null) { unchangedFKs.add(targetFK); } } return unchangedFKs; } /** * Adds the foreign keys of the unchanged tables that reference changed tables * to the given map. * * @param currentModel The current model * @param desiredModel The desired model * @param namesOfKnownChangedTables The known names of changed tables * @param fksPerTable The map table name -> foreign keys to which * found foreign keys will be added to */ private void addRelevantFKsFromUnchangedTables(Database currentModel, Database desiredModel, Set namesOfKnownChangedTables, Map fksPerTable) { boolean caseSensitive = getPlatform().isDelimitedIdentifierModeOn(); for (int tableIdx = 0; tableIdx < desiredModel.getTableCount(); tableIdx++) { Table targetTable = desiredModel.getTable(tableIdx); String name = targetTable.getName(); Table sourceTable = currentModel.findTable(name, caseSensitive); List relevantFks = null; if (!caseSensitive) { name = name.toUpperCase(); } if ((sourceTable != null) && !namesOfKnownChangedTables.contains(name)) { for (int fkIdx = 0; fkIdx < targetTable.getForeignKeyCount(); fkIdx++) { ForeignKey targetFk = targetTable.getForeignKey(fkIdx); ForeignKey sourceFk = sourceTable.findForeignKey(targetFk, caseSensitive); String refName = targetFk.getForeignTableName(); if (!caseSensitive) { refName = refName.toUpperCase(); } if ((sourceFk != null) && namesOfKnownChangedTables.contains(refName)) { if (relevantFks == null) { relevantFks = new ArrayList(); fksPerTable.put(name, relevantFks); } relevantFks.add(targetFk); } } } } } /** * Processes the changes to the structure of a single table. Database-specific * implementations might redefine this method, but it is usually sufficient to * redefine the {@link #processTableStructureChanges(Database, Database, Table, Table, Map, List)} * method instead. * * @param currentModel The current database schema * @param desiredModel The desired database schema * @param tableName The name of the changed table * @param parameters The creation parameters for the desired table * @param changes The change objects for this table */ protected void processTableStructureChanges(Database currentModel, Database desiredModel, String tableName, Map parameters, List changes) throws IOException { Table sourceTable = currentModel.findTable(tableName, getPlatform().isDelimitedIdentifierModeOn()); Table targetTable = desiredModel.findTable(tableName, getPlatform().isDelimitedIdentifierModeOn()); // we're enforcing a full rebuild in case of the addition of a required // column without a default value that is not autoincrement boolean requiresFullRebuild = false; for (Iterator changeIt = changes.iterator(); !requiresFullRebuild && changeIt.hasNext();) { TableChange change = (TableChange)changeIt.next(); if (change instanceof AddColumnChange) { AddColumnChange addColumnChange = (AddColumnChange)change; if (addColumnChange.getNewColumn().isRequired() && (addColumnChange.getNewColumn().getDefaultValue() == null) && !addColumnChange.getNewColumn().isAutoIncrement()) { requiresFullRebuild = true; } } } if (!requiresFullRebuild) { processTableStructureChanges(currentModel, desiredModel, sourceTable, targetTable, parameters, changes); } if (!changes.isEmpty()) { // 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 = changes.iterator(); canMigrateData && it.hasNext();) { TableChange change = (TableChange)it.next(); if (change instanceof AddColumnChange) { AddColumnChange addColumnChange = (AddColumnChange)change; if (addColumnChange.getNewColumn().isRequired() && !addColumnChange.getNewColumn().isAutoIncrement() && (addColumnChange.getNewColumn().getDefaultValue() == null)) { _log.warn("Data cannot be retained in table " + change.getChangedTable().getName() + " because of the addition of the required column " + addColumnChange.getNewColumn().getName()); canMigrateData = false; } } } Table realTargetTable = getRealTargetTableFor(desiredModel, sourceTable, targetTable); if (canMigrateData) { Table tempTable = getTemporaryTableFor(desiredModel, targetTable); createTemporaryTable(desiredModel, tempTable, parameters); writeCopyDataStatement(sourceTable, 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 dropTable(sourceTable); createTable(desiredModel, realTargetTable, parameters); writeCopyDataStatement(tempTable, targetTable); dropTemporaryTable(desiredModel, tempTable); writeFixLastIdentityValues(targetTable); } else { dropTable(sourceTable); createTable(desiredModel, realTargetTable, parameters); } } } /** * Allows database-specific implementations to handle changes in a database * specific manner. Any handled change should be applied to the given current * model (which is a copy of the real original model) and be removed from the * list of changes.
* In the default implementation, all {@link AddPrimaryKeyChange} changes are * applied via an ALTER TABLE ADD CONSTRAINT statement. * * @param currentModel The current database schema * @param desiredModel The desired database schema * @param sourceTable The original table * @param targetTable The desired table * @param parameters The creation parameters for the table * @param changes The change objects for the target table */ protected void processTableStructureChanges(Database currentModel, Database desiredModel, Table sourceTable, Table targetTable, Map parameters, List changes) throws IOException { if (changes.size() == 1) { TableChange change = (TableChange)changes.get(0); if (change instanceof AddPrimaryKeyChange) { processChange(currentModel, desiredModel, (AddPrimaryKeyChange)change); changes.clear(); } } } /** * 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 targetModel The target database * @param targetTable The target table * @return The temporary table */ protected Table getTemporaryTableFor(Database targetModel, Table targetTable) throws IOException { return getTemporaryTableFor(targetModel, targetTable, "_"); } public Table getBackupTableFor(Database model, Table sourceTable) throws IOException { return getTemporaryTableFor(model, sourceTable, "x"); } public Table createBackupTableFor(Database model, Table sourceTable) throws IOException { Table backupTable = getBackupTableFor(model, sourceTable); writeTableCreationStmt(model, backupTable, null); printEndOfStatement(); writeCopyDataStatement(sourceTable, backupTable); return backupTable; } public void restoreTableFromBackup(Table backupTable, Table targetTable, ListOrderedMap columnMap) throws IOException { print("DELETE FROM "); printIdentifier(getTableName(targetTable)); printEndOfStatement(); writeCopyDataStatement(backupTable, targetTable, columnMap); } protected Table getTemporaryTableFor(Database targetModel, Table targetTable, String suffix) throws IOException { Table table = new Table(); table.setCatalog(targetTable.getCatalog()); table.setSchema(targetTable.getSchema()); table.setName(targetTable.getName() + suffix); table.setType(targetTable.getType()); for (int idx = 0; idx < targetTable.getColumnCount(); idx++) { try { table.addColumn((Column)targetTable.getColumn(idx).clone()); } catch (CloneNotSupportedException ex) { throw new DdlUtilsException(ex); } } return table; } /** * Outputs the DDL to create the given temporary table. Per default this is simply * a call to {@link #createTable(Database, Table, Map)}. * * @param database The database model * @param table The table * @param parameters Additional platform-specific parameters for the table creation */ protected void createTemporaryTable(Database database, Table table, Map parameters) throws IOException { createTable(database, table, parameters); } /** * Outputs the DDL to drop the given temporary table. Per default this is simply * a call to {@link #dropTable(Table)}. * * @param database The database model * @param table The table */ protected void dropTemporaryTable(Database database, Table table) throws IOException { dropTable(table); } /** * Creates the target table object that differs from the given target table only in the * indices. More specifically, only those indices are used that have not changed. * * @param targetModel The target database * @param sourceTable The source table * @param targetTable The target table * @return The table */ protected Table getRealTargetTableFor(Database targetModel, Table sourceTable, Table targetTable) throws IOException { 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++) { try { table.addColumn((Column)targetTable.getColumn(idx).clone()); } catch (CloneNotSupportedException ex) { throw new DdlUtilsException(ex); } } boolean caseSensitive = getPlatform().isDelimitedIdentifierModeOn(); for (int idx = 0; idx < targetTable.getIndexCount(); idx++) { Index targetIndex = targetTable.getIndex(idx); Index sourceIndex = sourceTable.findIndex(targetIndex.getName(), caseSensitive); if (sourceIndex != null) { if ((caseSensitive && sourceIndex.equals(targetIndex)) || (!caseSensitive && sourceIndex.equalsIgnoreCase(targetIndex))) { table.addIndex(targetIndex); } } } return table; } /** * Writes a statement that copies the data from the source to the target table. Note * that this copies only those columns that are in both tables. * Database-specific implementations might redefine this method though they usually * it suffices to redefine the {@link #writeCastExpression(Column, Column)} method. * * @param sourceTable The source table * @param targetTable The target table */ public void writeCopyDataStatement(Table sourceTable, Table targetTable) throws IOException { ListOrderedMap columnMap = getCopyDataColumnMapping(sourceTable, targetTable); writeCopyDataStatement(sourceTable, targetTable, columnMap); } public void writeCopyDataStatement(Table sourceTable, Table targetTable, ListOrderedMap columnMap) throws IOException { print("INSERT INTO "); printIdentifier(getTableName(targetTable)); print(" ("); for (Iterator columnIt = columnMap.values().iterator(); columnIt.hasNext();) { printIdentifier(getColumnName((Column)columnIt.next())); if (columnIt.hasNext()) { print(","); } } print(") SELECT "); for (Iterator columnsIt = columnMap.entrySet().iterator(); columnsIt.hasNext();) { Map.Entry entry = (Map.Entry)columnsIt.next(); writeCastExpression((Column)entry.getKey(), (Column)entry.getValue()); if (columnsIt.hasNext()) { print(","); } } print(" FROM "); printIdentifier(getTableName(sourceTable)); printEndOfStatement(); } public ListOrderedMap getCopyDataColumnMapping(Table sourceTable, Table targetTable) { ListOrderedMap columns = new ListOrderedMap(); for (int idx = 0; idx < sourceTable.getColumnCount(); idx++) { Column sourceColumn = sourceTable.getColumn(idx); Column targetColumn = targetTable.findColumn(sourceColumn.getName(), getPlatform().isDelimitedIdentifierModeOn()); if (targetColumn != null) { columns.put(sourceColumn, targetColumn); } } return columns; } public ListOrderedMap getCopyDataColumnOrderedMapping(Table sourceTable, Table targetTable) { ListOrderedMap columns = new ListOrderedMap(); for (int idx = 0; idx < sourceTable.getColumnCount(); idx++) { columns.put(sourceTable.getColumn(idx), targetTable.getColumn(idx)); } return columns; } /** * Writes a cast expression that converts the value of the source column to the data type * of the target column. Per default, simply the name of the source column is written * thereby assuming that any casts happen implicitly. * * @param sourceColumn The source column * @param targetColumn The target column */ protected void writeCastExpression(Column sourceColumn, Column targetColumn) throws IOException { printIdentifier(getColumnName(sourceColumn)); } /** * Processes the addition of a primary key to a table. * * @param currentModel The current database schema * @param desiredModel The desired database schema * @param change The change object */ protected void processChange(Database currentModel, Database desiredModel, AddPrimaryKeyChange change) throws IOException { writeExternalPrimaryKeysCreateStmt(change.getChangedTable(), change.getPrimaryKeyColumns()); change.apply(currentModel, getPlatform().isDelimitedIdentifierModeOn()); } /** * Searches in the given table for a corresponding foreign key. If the given key * has no name, then a foreign key to the same table with the same columns in the * same order is searched. If the given key has a name, then the a corresponding * key also needs to have the same name, or no name at all, but not a different one. * * @param table The table to search in * @param fk The original foreign key * @return The corresponding foreign key if found */ protected ForeignKey findCorrespondingForeignKey(Table table, ForeignKey fk) { boolean caseMatters = getPlatform().isDelimitedIdentifierModeOn(); boolean checkFkName = (fk.getName() != null) && (fk.getName().length() > 0); Reference[] refs = fk.getReferences(); ArrayList curRefs = new ArrayList(); for (int fkIdx = 0; fkIdx < table.getForeignKeyCount(); fkIdx++) { ForeignKey curFk = table.getForeignKey(fkIdx); boolean checkCurFkName = checkFkName && (curFk.getName() != null) && (curFk.getName().length() > 0); if ((!checkCurFkName || areEqual(fk.getName(), curFk.getName(), caseMatters)) && areEqual(fk.getForeignTableName(), curFk.getForeignTableName(), caseMatters)) { curRefs.clear(); CollectionUtils.addAll(curRefs, curFk.getReferences()); // the order is not fixed, so we have to take this long way if (curRefs.size() == refs.length) { for (int refIdx = 0; refIdx < refs.length; refIdx++) { boolean found = false; for (int curRefIdx = 0; !found && (curRefIdx < curRefs.size()); curRefIdx++) { Reference curRef = (Reference)curRefs.get(curRefIdx); if ((caseMatters && refs[refIdx].equals(curRef)) || (!caseMatters && refs[refIdx].equalsIgnoreCase(curRef))) { curRefs.remove(curRefIdx); found = true; } } } if (curRefs.isEmpty()) { return curFk; } } } } return null; } /** * Compares the two strings. * * @param string1 The first string * @param string2 The second string * @param caseMatters Whether case matters in the comparison * @return true if the string are equal */ protected boolean areEqual(String string1, String string2, boolean caseMatters) { return (caseMatters && string1.equals(string2)) || (!caseMatters && string1.equalsIgnoreCase(string2)); } /** * Outputs the DDL to create the table along with any non-external constraints as well * as with external primary keys and indices (but not foreign keys). * * @param database The database model * @param table The table */ public void createTable(Database database, Table table) throws IOException { createTable(database, table, null); } /** * Outputs the DDL to create the table along with any non-external constraints as well * as with external primary keys and indices (but not foreign keys). * * @param database The database model * @param table The table * @param parameters Additional platform-specific parameters for the table creation */ public void createTable(Database database, Table table, Map parameters) throws IOException { writeTableCreationStmt(database, table, parameters); writeTableCreationStmtEnding(table, parameters); if (!getPlatformInfo().isPrimaryKeyEmbedded()) { writeExternalPrimaryKeysCreateStmt(table, table.getPrimaryKeyColumns()); } if (!getPlatformInfo().isIndicesEmbedded()) { writeExternalIndicesCreateStmt(table); } } /** * Creates the external foreignkey creation statements for all tables in the database. * * @param database The database */ public void createExternalForeignKeys(Database database) throws IOException { for (int idx = 0; idx < database.getTableCount(); idx++) { createExternalForeignKeys(database, database.getTable(idx)); } } /** * Creates external foreignkey creation statements if necessary. * * @param database The database model * @param table The table */ public void createExternalForeignKeys(Database database, Table table) throws IOException { if (!getPlatformInfo().isForeignKeysEmbedded()) { for (int idx = 0; idx < table.getForeignKeyCount(); idx++) { writeExternalForeignKeyCreateStmt(database, table, table.getForeignKey(idx)); } } } /** * Outputs the DDL required to drop the database. * * @param database The database */ public void dropTables(Database database) throws IOException { // we're dropping the external foreignkeys first for (int idx = database.getTableCount() - 1; idx >= 0; idx--) { Table table = database.getTable(idx); if ((table.getName() != null) && (table.getName().length() > 0)) { dropExternalForeignKeys(table); } } // Next we drop the tables in reverse order to avoid referencial problems // TODO: It might be more useful to either (or both) // * determine an order in which the tables can be dropped safely (via the foreignkeys) // * alter the tables first to drop the internal foreignkeys for (int idx = database.getTableCount() - 1; idx >= 0; idx--) { Table table = database.getTable(idx); if ((table.getName() != null) && (table.getName().length() > 0)) { writeTableComment(table); dropTable(table); } } } /** * Outputs the DDL required to drop the given table. This method also * drops foreign keys to the table. * * @param database The database * @param table The table */ public void dropTable(Database database, Table table) throws IOException { // we're dropping the foreignkeys to the table first for (int idx = database.getTableCount() - 1; idx >= 0; idx--) { Table otherTable = database.getTable(idx); ForeignKey[] fks = otherTable.getForeignKeys(); for (int fkIdx = 0; (fks != null) && (fkIdx < fks.length); fkIdx++) { if (fks[fkIdx].getForeignTable().equals(table)) { writeExternalForeignKeyDropStmt(otherTable, fks[fkIdx]); } } } // and the foreign keys from the table dropExternalForeignKeys(table); writeTableComment(table); dropTable(table); } /** * Outputs the DDL to drop the table. Note that this method does not drop * foreign keys to this table. Use {@link #dropTable(Database, Table)} * if you want that. * * @param table The table to drop */ public void dropTable(Table table) throws IOException { print("DROP TABLE "); printIdentifier(getTableName(table)); printEndOfStatement(); } /** * Creates external foreignkey drop statements. * * @param table The table */ public void dropExternalForeignKeys(Table table) throws IOException { if (!getPlatformInfo().isForeignKeysEmbedded()) { for (int idx = 0; idx < table.getForeignKeyCount(); idx++) { writeExternalForeignKeyDropStmt(table, table.getForeignKey(idx)); } } } /** * Creates the SQL for inserting an object into the specified table. * If values are given then a concrete insert statement is created, otherwise an * insert statement usable in a prepared statement is build. * * @param table The table * @param columnValues The columns values indexed by the column names * @param genPlaceholders Whether to generate value placeholders for a * prepared statement * @return The insertion sql */ public String getInsertSql(Table table, Map columnValues, boolean genPlaceholders) { StringBuffer buffer = new StringBuffer("INSERT INTO "); boolean addComma = false; buffer.append(getDelimitedIdentifier(getTableName(table))); buffer.append(" ("); for (int idx = 0; idx < table.getColumnCount(); idx++) { Column column = table.getColumn(idx); if (columnValues.containsKey(column.getName())) { if (addComma) { buffer.append(", "); } buffer.append(getDelimitedIdentifier(column.getName())); addComma = true; } } buffer.append(") VALUES ("); if (genPlaceholders) { addComma = false; for (int idx = 0; idx < columnValues.size(); idx++) { if (addComma) { buffer.append(", "); } buffer.append("?"); addComma = true; } } else { addComma = false; for (int idx = 0; idx < table.getColumnCount(); idx++) { Column column = table.getColumn(idx); if (columnValues.containsKey(column.getName())) { if (addComma) { buffer.append(", "); } buffer.append(getValueAsString(column, columnValues.get(column.getName()))); addComma = true; } } } buffer.append(")"); return buffer.toString(); } /** * Creates the SQL for updating an object in the specified table. * If values are given then a concrete update statement is created, otherwise an * update statement usable in a prepared statement is build. * * @param table The table * @param columnValues Contains the values for the columns to update, and should also * contain the primary key values to identify the object to update * in case genPlaceholders is false * @param genPlaceholders Whether to generate value placeholders for a * prepared statement (both for the pk values and the object values) * @return The update sql */ public String getUpdateSql(Table table, Map columnValues, boolean genPlaceholders) { StringBuffer buffer = new StringBuffer("UPDATE "); boolean addSep = false; buffer.append(getDelimitedIdentifier(getTableName(table))); buffer.append(" SET "); for (int idx = 0; idx < table.getColumnCount(); idx++) { Column column = table.getColumn(idx); if (!column.isPrimaryKey() && columnValues.containsKey(column.getName())) { if (addSep) { buffer.append(", "); } buffer.append(getDelimitedIdentifier(column.getName())); buffer.append(" = "); if (genPlaceholders) { buffer.append("?"); } else { buffer.append(getValueAsString(column, columnValues.get(column.getName()))); } addSep = true; } } buffer.append(" WHERE "); addSep = false; for (int idx = 0; idx < table.getColumnCount(); idx++) { Column column = table.getColumn(idx); if (column.isPrimaryKey() && columnValues.containsKey(column.getName())) { if (addSep) { buffer.append(" AND "); } buffer.append(getDelimitedIdentifier(column.getName())); buffer.append(" = "); if (genPlaceholders) { buffer.append("?"); } else { buffer.append(getValueAsString(column, columnValues.get(column.getName()))); } addSep = true; } } return buffer.toString(); } /** * Creates the SQL for deleting an object from the specified table. * If values are given then a concrete delete statement is created, otherwise an * delete statement usable in a prepared statement is build. * * @param table The table * @param pkValues The primary key values indexed by the column names, can be empty * @param genPlaceholders Whether to generate value placeholders for a * prepared statement * @return The delete sql */ public String getDeleteSql(Table table, Map pkValues, boolean genPlaceholders) { StringBuffer buffer = new StringBuffer("DELETE FROM "); boolean addSep = false; buffer.append(getDelimitedIdentifier(getTableName(table))); if ((pkValues != null) && !pkValues.isEmpty()) { buffer.append(" WHERE "); for (Iterator it = pkValues.entrySet().iterator(); it.hasNext();) { Map.Entry entry = (Map.Entry)it.next(); Column column = table.findColumn((String)entry.getKey()); if (addSep) { buffer.append(" AND "); } buffer.append(getDelimitedIdentifier(entry.getKey().toString())); buffer.append(" = "); if (genPlaceholders) { buffer.append("?"); } else { buffer.append(column == null ? entry.getValue() : getValueAsString(column, entry.getValue())); } addSep = true; } } return buffer.toString(); } /** * Generates the string representation of the given value. * * @param column The column * @param value The value * @return The string representation */ protected String getValueAsString(Column column, Object value) { if (value == null) { return "NULL"; } StringBuffer result = new StringBuffer(); // TODO: Handle binary types (BINARY, VARBINARY, LONGVARBINARY, BLOB) switch (column.getTypeCode()) { case Types.DATE: result.append(getPlatformInfo().getValueQuoteToken()); if (!(value instanceof String) && (getValueDateFormat() != null)) { // TODO: Can the format method handle java.sql.Date properly ? result.append(getValueDateFormat().format(value)); } else { result.append(value.toString()); } result.append(getPlatformInfo().getValueQuoteToken()); break; case Types.TIME: result.append(getPlatformInfo().getValueQuoteToken()); if (!(value instanceof String) && (getValueTimeFormat() != null)) { // TODO: Can the format method handle java.sql.Date properly ? result.append(getValueTimeFormat().format(value)); } else { result.append(value.toString()); } result.append(getPlatformInfo().getValueQuoteToken()); break; case Types.TIMESTAMP: result.append(getPlatformInfo().getValueQuoteToken()); // TODO: SimpleDateFormat does not support nano seconds so we would // need a custom date formatter for timestamps result.append(value.toString()); result.append(getPlatformInfo().getValueQuoteToken()); break; case Types.REAL: case Types.NUMERIC: case Types.FLOAT: case Types.DOUBLE: case Types.DECIMAL: result.append(getPlatformInfo().getValueQuoteToken()); if (!(value instanceof String) && (getValueNumberFormat() != null)) { result.append(getValueNumberFormat().format(value)); } else { result.append(value.toString()); } result.append(getPlatformInfo().getValueQuoteToken()); break; default: result.append(getPlatformInfo().getValueQuoteToken()); result.append(escapeStringValue(value.toString())); result.append(getPlatformInfo().getValueQuoteToken()); break; } return result.toString(); } /** * Generates the SQL for querying the id that was created in the last insertion * operation. This is obviously only useful for pk fields that are auto-incrementing. * A database that does not support this, will return null. * * @param table The table * @return The sql, or null if the database does not support this */ public String getSelectLastIdentityValues(Table table) { // No default possible as the databases are quite different in this respect return null; } /** * Generates the SQL to execute that sets the current sequence number. * * @param table * @param id * @return */ public String fixLastIdentityValues(Table table) { return null; } public void writeFixLastIdentityValues(Table table) throws IOException { String sql = fixLastIdentityValues(table); if (sql != null) { print(sql); printEndOfStatement(); } } // // implementation methods that may be overridden by specific database builders // /** * Generates a version of the name that has at most the specified * length. * * @param name The original name * @param desiredLength The desired maximum length * @return The shortened version */ public String shortenName(String name, int desiredLength) { // TODO: Find an algorithm that generates unique names int originalLength = name.length(); if ((desiredLength <= 0) || (originalLength <= desiredLength)) { return name; } int delta = originalLength - desiredLength; int startCut = desiredLength / 2; StringBuffer result = new StringBuffer(); result.append(name.substring(0, startCut)); if (((startCut == 0) || (name.charAt(startCut - 1) != '_')) && ((startCut + delta + 1 == originalLength) || (name.charAt(startCut + delta + 1) != '_'))) { // just to make sure that there isn't already a '_' right before or right // after the cutting place (which would look odd with an aditional one) result.append("_"); } result.append(name.substring(startCut + delta + 1, originalLength)); return result.toString(); } /** * Returns the table name. This method takes care of length limitations imposed by some databases. * * @param table The table * @return The table name */ public String getTableName(Table table) { return getTableName(table.getName()); } public String getTableName(String tableName) { return shortenName(tableName, getMaxTableNameLength()); } /** * Outputs a comment for the table. * * @param table The table */ protected void writeTableComment(Table table) throws IOException { printComment("-----------------------------------------------------------------------"); printComment(getTableName(table)); printComment("-----------------------------------------------------------------------"); println(); } /** * Generates the first part of the ALTER TABLE statement including the * table name. * * @param table The table being altered */ protected void writeTableAlterStmt(Table table) throws IOException { print("ALTER TABLE "); printlnIdentifier(getTableName(table)); printIndent(); } /** * Writes the table creation statement without the statement end. * * @param database The model * @param table The table * @param parameters Additional platform-specific parameters for the table creation */ protected void writeTableCreationStmt(Database database, Table table, Map parameters) throws IOException { print("CREATE TABLE "); printlnIdentifier(getTableName(table)); println("("); writeColumns(table); if (getPlatformInfo().isPrimaryKeyEmbedded()) { writeEmbeddedPrimaryKeysStmt(table); } if (getPlatformInfo().isForeignKeysEmbedded()) { writeEmbeddedForeignKeysStmt(database, table); } if (getPlatformInfo().isIndicesEmbedded()) { writeEmbeddedIndicesStmt(table); } println(); print(")"); } /** * Writes the end of the table creation statement. Per default, * only the end of the statement is written, but this can be changed * in subclasses. * * @param table The table * @param parameters Additional platform-specific parameters for the table creation */ protected void writeTableCreationStmtEnding(Table table, Map parameters) throws IOException { printEndOfStatement(); } /** * Writes the columns of the given table. * * @param table The table */ protected void writeColumns(Table table) throws IOException { for (int idx = 0; idx < table.getColumnCount(); idx++) { printIndent(); writeColumn(table, table.getColumn(idx)); if (idx < table.getColumnCount() - 1) { println(","); } } } /** * Returns the column name. This method takes care of length limitations imposed by some databases. * * @param column The column * @return The column name */ protected String getColumnName(Column column) throws IOException { return shortenName(column.getName(), getMaxColumnNameLength()); } /** * Outputs the DDL for the specified column. * * @param table The table containing the column * @param column The column */ protected void writeColumn(Table table, Column column) throws IOException { //see comments in columnsDiffer about null/"" defaults printIdentifier(getColumnName(column)); print(" "); print(getSqlType(column)); writeColumnDefaultValueStmt(table, column); if (column.isRequired()) { print(" "); writeColumnNotNullableStmt(); } else if (getPlatformInfo().isNullAsDefaultValueRequired() && getPlatformInfo().hasNullDefault(column.getTypeCode())) { print(" "); writeColumnNullableStmt(); } if (column.isAutoIncrement() && !getPlatformInfo().isDefaultValueUsedForIdentitySpec()) { if (!getPlatformInfo().isNonPKIdentityColumnsSupported() && !column.isPrimaryKey()) { throw new ModelException("Column "+column.getName()+" in table "+table.getName()+" is auto-incrementing but not a primary key column, which is not supported by the platform"); } print(" "); writeColumnAutoIncrementStmt(table, column); } } /** * Returns the full SQL type specification (including size and precision/scale) for the * given column. * * @param column The column * @return The full SQL type string including the size */ protected String getSqlType(Column column) { String nativeType = getNativeType(column); int sizePos = nativeType.indexOf(SIZE_PLACEHOLDER); StringBuffer sqlType = new StringBuffer(); sqlType.append(sizePos >= 0 ? nativeType.substring(0, sizePos) : nativeType); Object sizeSpec = column.getSize(); if (sizeSpec == null) { sizeSpec = getPlatformInfo().getDefaultSize(column.getTypeCode()); } if (sizeSpec != null) { if (getPlatformInfo().hasSize(column.getTypeCode())) { sqlType.append("("); sqlType.append(sizeSpec.toString()); sqlType.append(")"); } else if (getPlatformInfo().hasPrecisionAndScale(column.getTypeCode())) { sqlType.append("("); sqlType.append(column.getSizeAsInt()); sqlType.append(","); sqlType.append(column.getScale()); sqlType.append(")"); } } sqlType.append(sizePos >= 0 ? nativeType.substring(sizePos + SIZE_PLACEHOLDER.length()) : ""); return sqlType.toString(); } /** * Returns the database-native type for the given column. * * @param column The column * @return The native type */ protected String getNativeType(Column column) { String nativeType = (String)getPlatformInfo().getNativeType(column.getTypeCode()); return nativeType == null ? column.getType() : nativeType; } /** * Returns the bare database-native type for the given column without any size specifies. * * @param column The column * @return The native type */ protected String getBareNativeType(Column column) { String nativeType = getNativeType(column); int sizePos = nativeType.indexOf(SIZE_PLACEHOLDER); return sizePos >= 0 ? nativeType.substring(0, sizePos) : nativeType; } /** * Returns the native default value for the column. * * @param column The column * @return The native default value */ protected String getNativeDefaultValue(Column column) { return column.getDefaultValue(); } /** * Escapes the necessary characters in given string value. * * @param value The value * @return The corresponding string with the special characters properly escaped */ protected String escapeStringValue(String value) { String result = value; for (Iterator it = _charSequencesToEscape.entrySet().iterator(); it.hasNext();) { Map.Entry entry = (Map.Entry)it.next(); result = StringUtils.replace(result, (String)entry.getKey(), (String)entry.getValue()); } return result; } /** * Determines whether the given default spec is a non-empty spec that shall be used in a DEFAULT * expression. E.g. if the spec is an empty string and the type is a numeric type, then it is * no valid default value whereas if it is a string type, then it is valid. * * @param defaultSpec The default value spec * @param typeCode The JDBC type code * @return true if the default value spec is valid */ protected boolean isValidDefaultValue(String defaultSpec, int typeCode) { return (defaultSpec != null) && ((defaultSpec.length() > 0) || (!TypeMap.isNumericType(typeCode) && !TypeMap.isDateTimeType(typeCode))); } /** * Prints the default value stmt part for the column. * * @param table The table * @param column The column */ protected void writeColumnDefaultValueStmt(Table table, Column column) throws IOException { Object parsedDefault = column.getParsedDefaultValue(); if (parsedDefault != null) { if (!getPlatformInfo().isDefaultValuesForLongTypesSupported() && ((column.getTypeCode() == Types.LONGVARBINARY) || (column.getTypeCode() == Types.LONGVARCHAR))) { throw new ModelException("The platform does not support default values for LONGVARCHAR or LONGVARBINARY columns"); } // we write empty default value strings only if the type is not a numeric or date/time type if (isValidDefaultValue(column.getDefaultValue(), column.getTypeCode())) { print(" DEFAULT "); writeColumnDefaultValue(table, column); } } else if (getPlatformInfo().isDefaultValueUsedForIdentitySpec() && column.isAutoIncrement()) { print(" DEFAULT "); writeColumnDefaultValue(table, column); } } /** * Prints the default value of the column. * * @param table The table * @param column The column */ protected void writeColumnDefaultValue(Table table, Column column) throws IOException { printDefaultValue(getNativeDefaultValue(column), column.getTypeCode()); } /** * Prints the default value of the column. * * @param defaultValue The default value * @param typeCode The type code to write the default value for */ protected void printDefaultValue(Object defaultValue, int typeCode) throws IOException { if (defaultValue != null) { boolean shouldUseQuotes = !TypeMap.isNumericType(typeCode); if (shouldUseQuotes) { // characters are only escaped when within a string literal print(getPlatformInfo().getValueQuoteToken()); print(escapeStringValue(defaultValue.toString())); print(getPlatformInfo().getValueQuoteToken()); } else { print(defaultValue.toString()); } } } /** * Prints that the column is an auto increment column. * * @param table The table * @param column The column */ protected void writeColumnAutoIncrementStmt(Table table, Column column) throws IOException { print("IDENTITY"); } /** * Prints that a column is nullable. */ protected void writeColumnNullableStmt() throws IOException { print("NULL"); } /** * Prints that a column is not nullable. */ protected void writeColumnNotNullableStmt() throws IOException { print("NOT NULL"); } /** * Compares the current column in the database with the desired one. * Type, nullability, size, scale, default value, and precision radix are * the attributes checked. Currently default values are compared, and * null and empty string are considered equal. * * @param currentColumn The current column as it is in the database * @param desiredColumn The desired column * @return true if the column specifications differ */ protected boolean columnsDiffer(Column currentColumn, Column desiredColumn) { //The createColumn method leaves off the default clause if column.getDefaultValue() //is null. mySQL interprets this as a default of "" or 0, and thus the columns //are always different according to this method. alterDatabase will generate //an alter statement for the column, but it will be the exact same definition //as before. In order to avoid this situation I am ignoring the comparison //if the desired default is null. In order to "un-default" a column you'll //have to have a default="" or default="0" in the schema xml. //If this is bad for other databases, it is recommended that the createColumn //method use a "DEFAULT NULL" statement if that is what is needed. //A good way to get this would be to require a defaultValue="" in the //schema xml if you really want null and not just unspecified. String desiredDefault = desiredColumn.getDefaultValue(); String currentDefault = currentColumn.getDefaultValue(); boolean defaultsEqual = (desiredDefault == null) || desiredDefault.equals(currentDefault); boolean sizeMatters = getPlatformInfo().hasSize(currentColumn.getTypeCode()) && (desiredColumn.getSize() != null); // We're comparing the jdbc type that corresponds to the native type for the // desired type, in order to avoid repeated altering of a perfectly valid column if ((getPlatformInfo().getTargetJdbcType(desiredColumn.getTypeCode()) != currentColumn.getTypeCode()) || (desiredColumn.isRequired() != currentColumn.isRequired()) || (sizeMatters && !StringUtils.equals(desiredColumn.getSize(), currentColumn.getSize())) || !defaultsEqual) { return true; } else { return false; } } /** * Returns the name to be used for the given foreign key. If the foreign key has no * specified name, this method determines a unique name for it. The name will also * be shortened to honor the maximum identifier length imposed by the platform. * * @param table The table for whith the foreign key is defined * @param fk The foreign key * @return The name */ public String getForeignKeyName(Table table, ForeignKey fk) { String fkName = fk.getName(); boolean needsName = (fkName == null) || (fkName.length() == 0); if (needsName) { StringBuffer name = new StringBuffer(); for (int idx = 0; idx < fk.getReferenceCount(); idx++) { name.append(fk.getReference(idx).getLocalColumnName()); name.append("_"); } name.append(fk.getForeignTableName()); fkName = getConstraintName(null, table, "FK", name.toString()); } fkName = shortenName(fkName, getMaxForeignKeyNameLength()); if (needsName) { _log.warn("Encountered a foreign key in table " + table.getName() + " that has no name. " + "DdlUtils will use the auto-generated and shortened name " + fkName + " instead."); } return fkName; } /** * Returns the constraint name. This method takes care of length limitations imposed by some databases. * * @param prefix The constraint prefix, can be null * @param table The table that the constraint belongs to * @param secondPart The second name part, e.g. the name of the constraint column * @param suffix The constraint suffix, e.g. a counter (can be null) * @return The constraint name */ public String getConstraintName(String prefix, Table table, String secondPart, String suffix) { StringBuffer result = new StringBuffer(); if (prefix != null) { result.append(prefix); result.append("_"); } result.append(table.getName()); result.append("_"); result.append(secondPart); if (suffix != null) { result.append("_"); result.append(suffix); } return shortenName(result.toString(), getMaxConstraintNameLength()); } /** * Writes the primary key constraints of the table inside its definition. * * @param table The table */ protected void writeEmbeddedPrimaryKeysStmt(Table table) throws IOException { Column[] primaryKeyColumns = table.getPrimaryKeyColumns(); if ((primaryKeyColumns.length > 0) && shouldGeneratePrimaryKeys(primaryKeyColumns)) { printStartOfEmbeddedStatement(); writePrimaryKeyStmt(table, primaryKeyColumns); } } /** * Writes the primary key constraints of the table as alter table statements. * * @param table The table * @param primaryKeyColumns The primary key columns */ protected void writeExternalPrimaryKeysCreateStmt(Table table, Column[] primaryKeyColumns) throws IOException { if ((primaryKeyColumns.length > 0) && shouldGeneratePrimaryKeys(primaryKeyColumns)) { print("ALTER TABLE "); printlnIdentifier(getTableName(table)); printIndent(); print("ADD CONSTRAINT "); printIdentifier(getConstraintName(null, table, "PK", null)); print(" "); writePrimaryKeyStmt(table, primaryKeyColumns); printEndOfStatement(); } } /** * Determines whether we should generate a primary key constraint for the given * primary key columns. * * @param primaryKeyColumns The pk columns * @return true if a pk statement should be generated for the columns */ protected boolean shouldGeneratePrimaryKeys(Column[] primaryKeyColumns) { return true; } /** * Writes a primary key statement for the given columns. * * @param table The table * @param primaryKeyColumns The primary columns */ protected void writePrimaryKeyStmt(Table table, Column[] primaryKeyColumns) throws IOException { print("PRIMARY KEY ("); for (int idx = 0; idx < primaryKeyColumns.length; idx++) { printIdentifier(getColumnName(primaryKeyColumns[idx])); if (idx < primaryKeyColumns.length - 1) { print(", "); } } print(")"); } /** * Returns the index name. This method takes care of length limitations imposed by some databases. * * @param index The index * @return The index name */ public String getIndexName(Index index) { return shortenName(index.getName(), getMaxConstraintNameLength()); } /** * Writes the indexes of the given table. * * @param table The table */ protected void writeExternalIndicesCreateStmt(Table table) throws IOException { for (int idx = 0; idx < table.getIndexCount(); idx++) { Index index = table.getIndex(idx); if (!index.isUnique() && !getPlatformInfo().isIndicesSupported()) { throw new ModelException("Platform does not support non-unique indices"); } writeExternalIndexCreateStmt(table, index); } } /** * Writes the indexes embedded within the create table statement. * * @param table The table */ protected void writeEmbeddedIndicesStmt(Table table) throws IOException { if (getPlatformInfo().isIndicesSupported()) { for (int idx = 0; idx < table.getIndexCount(); idx++) { printStartOfEmbeddedStatement(); writeEmbeddedIndexCreateStmt(table, table.getIndex(idx)); } } } /** * Writes the given index of the table. * * @param table The table * @param index The index */ protected void writeExternalIndexCreateStmt(Table table, Index index) throws IOException { if (getPlatformInfo().isIndicesSupported()) { if (index.getName() == null) { _log.warn("Cannot write unnamed index " + index); } else { print("CREATE"); if (index.isUnique()) { print(" UNIQUE"); } print(" INDEX "); printIdentifier(getIndexName(index)); print(" ON "); printIdentifier(getTableName(table)); print(" ("); for (int idx = 0; idx < index.getColumnCount(); idx++) { IndexColumn idxColumn = index.getColumn(idx); Column col = table.findColumn(idxColumn.getName()); if (col == null) { // would get null pointer on next line anyway, so throw exception throw new ModelException("Invalid column '" + idxColumn.getName() + "' on index " + index.getName() + " for table " + table.getName()); } if (idx > 0) { print(", "); } printIdentifier(getColumnName(col)); } print(")"); printEndOfStatement(); } } } /** * Writes the given embedded index of the table. * * @param table The table * @param index The index */ protected void writeEmbeddedIndexCreateStmt(Table table, Index index) throws IOException { if ((index.getName() != null) && (index.getName().length() > 0)) { print(" CONSTRAINT "); printIdentifier(getIndexName(index)); } if (index.isUnique()) { print(" UNIQUE"); } else { print(" INDEX "); } print(" ("); for (int idx = 0; idx < index.getColumnCount(); idx++) { IndexColumn idxColumn = index.getColumn(idx); Column col = table.findColumn(idxColumn.getName()); if (col == null) { // would get null pointer on next line anyway, so throw exception throw new ModelException("Invalid column '" + idxColumn.getName() + "' on index " + index.getName() + " for table " + table.getName()); } if (idx > 0) { print(", "); } printIdentifier(getColumnName(col)); } print(")"); } /** * Generates the statement to drop a non-embedded index from the database. * * @param table The table the index is on * @param index The index to drop */ public void writeExternalIndexDropStmt(Table table, Index index) throws IOException { if (getPlatformInfo().isAlterTableForDropUsed()) { writeTableAlterStmt(table); } print("DROP INDEX "); printIdentifier(getIndexName(index)); if (!getPlatformInfo().isAlterTableForDropUsed()) { print(" ON "); printIdentifier(getTableName(table)); } printEndOfStatement(); } /** * Writes the foreign key constraints inside a create table () clause. * * @param database The database model * @param table The table */ protected void writeEmbeddedForeignKeysStmt(Database database, Table table) throws IOException { for (int idx = 0; idx < table.getForeignKeyCount(); idx++) { ForeignKey key = table.getForeignKey(idx); if (key.getForeignTableName() == null) { _log.warn("Foreign key table is null for key " + key); } else { printStartOfEmbeddedStatement(); if (getPlatformInfo().isEmbeddedForeignKeysNamed()) { print("CONSTRAINT "); printIdentifier(getForeignKeyName(table, key)); print(" "); } print("FOREIGN KEY ("); writeLocalReferences(key); print(") REFERENCES "); printIdentifier(getTableName(database.findTable(key.getForeignTableName()))); print(" ("); writeForeignReferences(key); print(")"); } } } /** * Writes a single foreign key constraint using a alter table statement. * * @param database The database model * @param table The table * @param key The foreign key */ protected void writeExternalForeignKeyCreateStmt(Database database, Table table, ForeignKey key) throws IOException { if (key.getForeignTableName() == null) { _log.warn("Foreign key table is null for key " + key); } else { writeTableAlterStmt(table); print("ADD CONSTRAINT "); printIdentifier(getForeignKeyName(table, key)); print(" FOREIGN KEY ("); writeLocalReferences(key); print(") REFERENCES "); printIdentifier(getTableName(key.getForeignTableName())); print(" ("); writeForeignReferences(key); print(")"); printEndOfStatement(); } } /** * Writes a list of local references for the given foreign key. * * @param key The foreign key */ protected void writeLocalReferences(ForeignKey key) throws IOException { for (int idx = 0; idx < key.getReferenceCount(); idx++) { if (idx > 0) { print(", "); } printIdentifier(key.getReference(idx).getLocalColumnName()); } } /** * Writes a list of foreign references for the given foreign key. * * @param key The foreign key */ protected void writeForeignReferences(ForeignKey key) throws IOException { for (int idx = 0; idx < key.getReferenceCount(); idx++) { if (idx > 0) { print(", "); } printIdentifier(key.getReference(idx).getForeignColumnName()); } } /** * Generates the statement to drop a foreignkey constraint from the database using an * alter table statement. * * @param table The table * @param foreignKey The foreign key */ protected void writeExternalForeignKeyDropStmt(Table table, ForeignKey foreignKey) throws IOException { writeTableAlterStmt(table); print("DROP CONSTRAINT "); printIdentifier(getForeignKeyName(table, foreignKey)); printEndOfStatement(); } // // Helper methods // /** * Prints an SQL comment to the current stream. * * @param text The comment text */ protected void printComment(String text) throws IOException { if (getPlatform().isSqlCommentsOn()) { print(getPlatformInfo().getCommentPrefix()); // Some databases insist on a space after the prefix print(" "); print(text); print(" "); print(getPlatformInfo().getCommentSuffix()); println(); } } /** * Prints the start of an embedded statement. */ protected void printStartOfEmbeddedStatement() throws IOException { println(","); printIndent(); } /** * Prints the end of statement text, which is typically a semi colon followed by * a carriage return. */ protected void printEndOfStatement() throws IOException { // TODO: It might make sense to use a special writer which stores the individual // statements separately (the end of a statement is identified by this method) println(getPlatformInfo().getSqlCommandDelimiter()); println(); } /** * Prints a newline. */ protected void println() throws IOException { print(LINE_SEPARATOR); } /** * Prints some text. * * @param text The text to print */ protected void print(String text) throws IOException { _writer.write(text); } /** * Returns the delimited version of the identifier (if configured). * * @param identifier The identifier * @return The delimited version of the identifier unless the platform is configured * to use undelimited identifiers; in that case, the identifier is returned unchanged */ protected String getDelimitedIdentifier(String identifier) { if (getPlatform().isDelimitedIdentifierModeOn()) { return getPlatformInfo().getDelimiterToken() + identifier + getPlatformInfo().getDelimiterToken(); } else { return identifier; } } /** * Prints the given identifier. For most databases, this will * be a delimited identifier. * * @param identifier The identifier */ protected void printIdentifier(String identifier) throws IOException { print(getDelimitedIdentifier(identifier)); } /** * Prints the given identifier followed by a newline. For most databases, this will * be a delimited identifier. * * @param identifier The identifier */ protected void printlnIdentifier(String identifier) throws IOException { println(getDelimitedIdentifier(identifier)); } /** * Prints some text followed by a newline. * * @param text The text to print */ protected void println(String text) throws IOException { print(text); println(); } /** * Prints the characters used to indent SQL. */ protected void printIndent() throws IOException { print(getIndent()); } /** * Creates a reasonably unique identifier only consisting of hexadecimal characters and underscores. * It looks like d578271282b42fce__2955b56e_107df3fbc96__8000 and is 48 characters long. * * @return The identifier */ protected String createUniqueIdentifier() { return new UID().toString().replace(':', '_').replace('-', '_'); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy