| package org.apache.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 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.Iterator; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| 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.apache.ddlutils.DdlUtilsException; |
| import org.apache.ddlutils.Platform; |
| import org.apache.ddlutils.PlatformInfo; |
| import org.apache.ddlutils.model.CascadeActionEnum; |
| import org.apache.ddlutils.model.Column; |
| import org.apache.ddlutils.model.Database; |
| import org.apache.ddlutils.model.ForeignKey; |
| import org.apache.ddlutils.model.Index; |
| import org.apache.ddlutils.model.IndexColumn; |
| import org.apache.ddlutils.model.ModelException; |
| import org.apache.ddlutils.model.Table; |
| import org.apache.ddlutils.model.TypeMap; |
| import org.apache.ddlutils.util.StringUtilsExt; |
| |
| /** |
| * 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$ |
| */ |
| 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 <code>null</code> 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 <code>null</code> 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 |
| createForeignKeys(database); |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * 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 |
| */ |
| protected void copyData(Table sourceTable, Table targetTable) throws IOException |
| { |
| 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); |
| } |
| } |
| |
| print("INSERT INTO "); |
| printIdentifier(getTableName(targetTable)); |
| print(" ("); |
| for (Iterator columnIt = columns.keySet().iterator(); columnIt.hasNext();) |
| { |
| printIdentifier(getColumnName((Column)columnIt.next())); |
| if (columnIt.hasNext()) |
| { |
| print(","); |
| } |
| } |
| print(") SELECT "); |
| for (Iterator columnsIt = columns.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(); |
| } |
| |
| /** |
| * 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)); |
| } |
| |
| /** |
| * Compares the two strings. |
| * |
| * @param string1 The first string |
| * @param string2 The second string |
| * @param caseMatters Whether case matters in the comparison |
| * @return <code>true</code> 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()) |
| { |
| createPrimaryKey(table, table.getPrimaryKeyColumns()); |
| } |
| if (!getPlatformInfo().isIndicesEmbedded()) |
| { |
| createIndexes(table); |
| } |
| } |
| |
| /** |
| * Writes the primary key constraints of the table as alter table statements. |
| * |
| * @param table The table |
| * @param primaryKeyColumns The primary key columns |
| */ |
| public void createPrimaryKey(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(); |
| } |
| } |
| |
| /** |
| * Writes the indexes for the given table using external index creation statements. |
| * |
| * @param table The table |
| */ |
| public void createIndexes(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"); |
| } |
| createIndex(table, index); |
| } |
| } |
| |
| /** |
| * Writes the given index for the table using an external index creation statement. |
| * |
| * @param table The table |
| * @param index The index |
| */ |
| public void createIndex(Table table, Index index) throws IOException |
| { |
| if (!getPlatformInfo().isIndicesSupported()) |
| { |
| throw new DdlUtilsException("This platform does not support indexes"); |
| } |
| else 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(); |
| } |
| } |
| |
| /** |
| * Creates the external foreignkey creation statements for all tables in the database. |
| * |
| * @param database The database |
| */ |
| public void createForeignKeys(Database database) throws IOException |
| { |
| for (int idx = 0; idx < database.getTableCount(); idx++) |
| { |
| createForeignKeys(database, database.getTable(idx)); |
| } |
| } |
| |
| /** |
| * Creates external foreignkey creation statements if necessary. |
| * |
| * @param database The database model |
| * @param table The table |
| */ |
| public void createForeignKeys(Database database, Table table) throws IOException |
| { |
| for (int idx = 0; idx < table.getForeignKeyCount(); idx++) |
| { |
| createForeignKey(database, table, table.getForeignKey(idx)); |
| } |
| } |
| |
| /** |
| * Writes a single foreign key constraint using a alter table statement. |
| * |
| * @param database The database model |
| * @param table The table |
| * @param foreignKey The foreign key |
| */ |
| public void createForeignKey(Database database, Table table, ForeignKey foreignKey) throws IOException |
| { |
| if (getPlatformInfo().isForeignKeysEmbedded()) |
| { |
| throw new DdlUtilsException("This platform does not supported the external creation of foreign keys"); |
| } |
| else if (foreignKey.getForeignTableName() == null) |
| { |
| _log.warn("Foreign key table is null for key " + foreignKey); |
| } |
| else |
| { |
| writeTableAlterStmt(table); |
| |
| print("ADD CONSTRAINT "); |
| printIdentifier(getForeignKeyName(table, foreignKey)); |
| print(" FOREIGN KEY ("); |
| writeLocalReferences(foreignKey); |
| print(") REFERENCES "); |
| printIdentifier(getTableName(database.findTable(foreignKey.getForeignTableName()))); |
| print(" ("); |
| writeForeignReferences(foreignKey); |
| print(")"); |
| writeForeignKeyOnDeleteAction(table, foreignKey); |
| writeForeignKeyOnUpdateAction(table, foreignKey); |
| printEndOfStatement(); |
| } |
| } |
| |
| /** |
| * Prints the SQL for adding a column to a table. |
| * |
| * @param model The database model |
| * @param table The table |
| * @param newColumn The new column |
| */ |
| public void addColumn(Database model, Table table, Column newColumn) throws IOException |
| { |
| print("ALTER TABLE "); |
| printlnIdentifier(getTableName(table)); |
| printIndent(); |
| print("ADD COLUMN "); |
| writeColumn(table, newColumn); |
| printEndOfStatement(); |
| } |
| |
| /** |
| * 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)) |
| { |
| dropForeignKeys(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)) |
| { |
| dropForeignKey(otherTable, fks[fkIdx]); |
| } |
| } |
| } |
| // and the foreign keys from the table |
| dropForeignKeys(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 dropForeignKeys(Table table) throws IOException |
| { |
| for (int idx = 0; idx < table.getForeignKeyCount(); idx++) |
| { |
| dropForeignKey(table, table.getForeignKey(idx)); |
| } |
| } |
| |
| /** |
| * 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 |
| */ |
| public void dropForeignKey(Table table, ForeignKey foreignKey) throws IOException |
| { |
| writeTableAlterStmt(table); |
| print("DROP CONSTRAINT "); |
| printIdentifier(getForeignKeyName(table, foreignKey)); |
| printEndOfStatement(); |
| } |
| |
| /** |
| * 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 <code>genPlaceholders</code> is <code>false</code> |
| * @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 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 oldColumnValues Contains the column values to identify the row to update |
| * @param newColumnValues Contains the values for the columns to update |
| * @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 oldColumnValues, Map newColumnValues, 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 (newColumnValues.containsKey(column.getName())) |
| { |
| if (addSep) |
| { |
| buffer.append(", "); |
| } |
| buffer.append(getDelimitedIdentifier(column.getName())); |
| buffer.append(" = "); |
| if (genPlaceholders) |
| { |
| buffer.append("?"); |
| } |
| else |
| { |
| buffer.append(getValueAsString(column, newColumnValues.get(column.getName()))); |
| } |
| addSep = true; |
| } |
| } |
| buffer.append(" WHERE "); |
| addSep = false; |
| for (int idx = 0; idx < table.getColumnCount(); idx++) |
| { |
| Column column = table.getColumn(idx); |
| |
| if (oldColumnValues.containsKey(column.getName())) |
| { |
| if (addSep) |
| { |
| buffer.append(" AND "); |
| } |
| buffer.append(getDelimitedIdentifier(column.getName())); |
| buffer.append(" = "); |
| if (genPlaceholders) |
| { |
| buffer.append("?"); |
| } |
| else |
| { |
| buffer.append(getValueAsString(column, oldColumnValues.get(column.getName()))); |
| } |
| addSep = true; |
| } |
| } |
| return buffer.toString(); |
| } |
| |
| /** |
| * Creates the SQL for deleting an object from the specified table. Depending on |
| * the value of <code>genPlaceholders</code>, the generated SQL will contain |
| * prepared statement place holders or concrete values. Only those primary key |
| * columns wil be used that are present in the given map. If the map is null or |
| * completely empty, then the SQL will not have a WHERE clause. The SQL will contain |
| * the columns in the order defined in the table. |
| * |
| * @param table The table |
| * @param pkValues The primary key columns to use, and optionally their values |
| * @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 "); |
| |
| Column[] pkCols = table.getPrimaryKeyColumns(); |
| |
| for (int pkColIdx = 0; pkColIdx < pkCols.length; pkColIdx++) |
| { |
| Column column = pkCols[pkColIdx]; |
| |
| if (pkValues.containsKey(column.getName())) { |
| if (addSep) |
| { |
| buffer.append(" AND "); |
| } |
| buffer.append(getDelimitedIdentifier(column.getName())); |
| buffer.append(" = "); |
| if (genPlaceholders) |
| { |
| buffer.append("?"); |
| } |
| else |
| { |
| buffer.append(getValueAsString(column, pkValues.get(column.getName()))); |
| } |
| 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 <code>null</code>. |
| * |
| * @param table The table |
| * @return The sql, or <code>null</code> 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; |
| } |
| |
| // |
| // 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) |
| { |
| if (name == null) |
| { |
| return null; |
| } |
| |
| // 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 shortenName(table.getName(), 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().isNonPrimaryKeyIdentityColumnsSupported() && !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) |
| { |
| return getSqlType(column, getNativeType(column)); |
| } |
| |
| /** |
| * Returns the full SQL type specification (including size and precision/scale) for the |
| * given column. |
| * |
| * @param column The column |
| * @param nativeType Overrides the native type of the column; can include the size placeholder |
| * @return The full SQL type string including the size |
| */ |
| protected String getSqlType(Column column, String nativeType) |
| { |
| int sizePos = nativeType.indexOf(SIZE_PLACEHOLDER); |
| StringBuffer sqlType = new StringBuffer(); |
| |
| sqlType.append(sizePos >= 0 ? nativeType.substring(0, sizePos) : nativeType); |
| |
| String sizeSpec = getSizeSpec(column); |
| |
| if (!StringUtilsExt.isEmpty(sizeSpec)) |
| { |
| sqlType.append("("); |
| sqlType.append(sizeSpec); |
| 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 size specification for the given column. If the column is of a type that has size |
| * or precision and scale, and no size is defined for the column itself, then the default size |
| * or precision/scale for that type and platform is used instead. |
| * |
| * @param column The column |
| * @return The size spec |
| */ |
| protected String getSizeSpec(Column column) |
| { |
| StringBuffer result = new StringBuffer(); |
| Object sizeSpec = column.getSize(); |
| |
| if (sizeSpec == null) |
| { |
| sizeSpec = getPlatformInfo().getDefaultSize(column.getTypeCode()); |
| } |
| if (sizeSpec != null) |
| { |
| if (getPlatformInfo().hasSize(column.getTypeCode())) |
| { |
| result.append(sizeSpec.toString()); |
| } |
| else if (getPlatformInfo().hasPrecisionAndScale(column.getTypeCode())) |
| { |
| result.append(column.getSizeAsInt()); |
| result.append(","); |
| result.append(column.getScale()); |
| } |
| } |
| return result.toString(); |
| } |
| |
| /** |
| * 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 <code>true</code> 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 <code>true</code> 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="<NULL>" 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 <code>null</code> |
| * @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 <code>null</code>) |
| * @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); |
| } |
| } |
| |
| /** |
| * Determines whether we should generate a primary key constraint for the given |
| * primary key columns. |
| * |
| * @param primaryKeyColumns The pk columns |
| * @return <code>true</code> 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 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 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 dropIndex(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 foreignKey = table.getForeignKey(idx); |
| |
| if (foreignKey.getForeignTableName() == null) |
| { |
| _log.warn("Foreign key table is null for key " + foreignKey); |
| } |
| else |
| { |
| printStartOfEmbeddedStatement(); |
| if (getPlatformInfo().isEmbeddedForeignKeysNamed()) |
| { |
| print("CONSTRAINT "); |
| printIdentifier(getForeignKeyName(table, foreignKey)); |
| print(" "); |
| } |
| print("FOREIGN KEY ("); |
| writeLocalReferences(foreignKey); |
| print(") REFERENCES "); |
| printIdentifier(getTableName(database.findTable(foreignKey.getForeignTableName()))); |
| print(" ("); |
| writeForeignReferences(foreignKey); |
| print(")"); |
| writeForeignKeyOnDeleteAction(table, foreignKey); |
| writeForeignKeyOnUpdateAction(table, foreignKey); |
| } |
| } |
| } |
| |
| /** |
| * 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()); |
| } |
| } |
| |
| /** |
| * Writes the onDelete action for the given foreign key. |
| * |
| * @param table The table |
| * @param foreignKey The foreignkey |
| */ |
| private void writeForeignKeyOnDeleteAction(Table table, ForeignKey foreignKey) throws IOException |
| { |
| CascadeActionEnum action = foreignKey.getOnDelete(); |
| |
| if (!getPlatformInfo().isActionSupportedForOnDelete(action)) |
| { |
| if (getPlatform().isDefaultOnDeleteActionUsedIfUnsupported()) |
| { |
| _log.info("The platform does not support the " + action + " action for onDelete; using " + getPlatformInfo().getDefaultOnDeleteAction() + " instead"); |
| action = getPlatformInfo().getDefaultOnDeleteAction(); |
| } |
| else |
| { |
| throw new ModelException("The platform does not support the action '" + action + |
| "' for onDelete in foreign key in table " + table.getName()); |
| } |
| } |
| if (action != getPlatformInfo().getDefaultOnDeleteAction()) |
| { |
| print(" ON DELETE "); |
| switch (action.getValue()) |
| { |
| case CascadeActionEnum.VALUE_CASCADE: |
| print("CASCADE"); |
| break; |
| case CascadeActionEnum.VALUE_SET_NULL: |
| print("SET NULL"); |
| break; |
| case CascadeActionEnum.VALUE_SET_DEFAULT: |
| print("SET DEFAULT"); |
| break; |
| case CascadeActionEnum.VALUE_RESTRICT: |
| print("RESTRICT"); |
| break; |
| case CascadeActionEnum.VALUE_NONE: |
| print("NO ACTION"); |
| break; |
| default: |
| throw new ModelException("Unsupported cascade value '" + action + |
| "' for onDelete in foreign key in table " + table.getName()); |
| } |
| } |
| } |
| |
| /** |
| * Writes the onDelete action for the given foreign key. |
| * |
| * @param table The table |
| * @param foreignKey The foreignkey |
| */ |
| private void writeForeignKeyOnUpdateAction(Table table, ForeignKey foreignKey) throws IOException |
| { |
| CascadeActionEnum action = foreignKey.getOnUpdate(); |
| |
| if (!getPlatformInfo().isActionSupportedForOnUpdate(action)) |
| { |
| if (getPlatform().isDefaultOnUpdateActionUsedIfUnsupported()) |
| { |
| _log.info("The platform does not support the " + action + " action for onUpdate; using " + getPlatformInfo().getDefaultOnUpdateAction() + " instead"); |
| action = getPlatformInfo().getDefaultOnUpdateAction(); |
| } |
| else |
| { |
| throw new ModelException("The platform does not support the action '" + action + |
| "' for onUpdate in foreign key in table " + table.getName()); |
| } |
| } |
| if (action != getPlatformInfo().getDefaultOnUpdateAction()) |
| { |
| print(" ON UPDATE "); |
| switch (action.getValue()) |
| { |
| case CascadeActionEnum.VALUE_CASCADE: |
| print("CASCADE"); |
| break; |
| case CascadeActionEnum.VALUE_SET_NULL: |
| print("SET NULL"); |
| break; |
| case CascadeActionEnum.VALUE_SET_DEFAULT: |
| print("SET DEFAULT"); |
| break; |
| case CascadeActionEnum.VALUE_RESTRICT: |
| print("RESTRICT"); |
| break; |
| case CascadeActionEnum.VALUE_NONE: |
| print("NO ACTION"); |
| break; |
| default: |
| throw new ModelException("Unsupported cascade value '" + action + |
| "' for onUpdate in foreign key in table " + table.getName()); |
| } |
| } |
| } |
| |
| // |
| // 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 <code>d578271282b42fce__2955b56e_107df3fbc96__8000</code> and is 48 characters long. |
| * |
| * @return The identifier |
| */ |
| protected String createUniqueIdentifier() |
| { |
| return new UID().toString().replace(':', '_').replace('-', '_'); |
| } |
| } |