/* * Copyright (c) 2008, SQL Power Group Inc. * * This file is part of SQL Power Library. * * SQL Power Library is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * SQL Power Library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.sqlpower.sqlobject; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.log4j.Logger; import ca.sqlpower.object.ObjectDependentException; import ca.sqlpower.object.SPObject; import ca.sqlpower.object.SPVariableResolver; import ca.sqlpower.object.SPVariableResolverProvider; import ca.sqlpower.object.annotation.Accessor; import ca.sqlpower.object.annotation.Constructor; import ca.sqlpower.object.annotation.ConstructorParameter; import ca.sqlpower.object.annotation.Mutator; import ca.sqlpower.object.annotation.NonBound; import ca.sqlpower.object.annotation.NonProperty; import ca.sqlpower.object.annotation.Transient; import ca.sqlpower.object.annotation.ConstructorParameter.ParameterType; import ca.sqlpower.sql.DataSourceCollection; import ca.sqlpower.sql.JDBCDataSourceType; import ca.sqlpower.sql.SPDataSource; import ca.sqlpower.sql.SQL; import ca.sqlpower.sqlobject.SQLRelationship.SQLImportedKey; import ca.sqlpower.sqlobject.SQLTypePhysicalProperties.SQLTypeConstraint; import ca.sqlpower.sqlobject.SQLTypePhysicalPropertiesProvider.PropertyType; import ca.sqlpower.swingui.ModalDialogListUserPrompter; import ca.sqlpower.util.UserPrompter; import ca.sqlpower.util.UserPrompterFactory; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; public class SQLColumn extends SQLObject implements java.io.Serializable, SPVariableResolverProvider, SQLCheckConstraintContainer { private static Logger logger = Logger.getLogger(SQLColumn.class); public static final List<Class<? extends SPObject>> allowedChildTypes = Collections.<Class<? extends SPObject>>singletonList(UserDefinedSQLType.class); protected final UserDefinedSQLType userDefinedSQLType; protected String platform; // *** REMEMBER *** update the copyProperties method if you add new properties! /** * Refers back to the real database-connected SQLColumn that this * column was originally derived from. */ protected SQLColumn sourceColumn; // set to empty string so that we don't generate spurious undos protected String remarks =""; /** * This property is a hint to the DDL generators to tell them what name * to give to the database sequence that generates values for this column. * Not all platforms need (or support) sequences, so setting this value * doesn't guarantee it will be used. If the value of this field is left * null, the getter method will auto-generate a sequence name based on * the table and column names. */ private String autoIncrementSequenceName; /** * @see #getEtlTransformationLogic() */ private String etlTransformationLogic; /** * @see #getEtlDefaultValue() */ private String etlDefaultValue; /** * @see #getEtlAuditable() */ private Boolean etlAuditable; /** * @see #getEtlNotes() */ private String etlNotes; // *** REMEMBER *** update the copyProperties method if you add new properties! // These are not specific Column properties.Instead,they are default column // user settings. private static String defaultName; private static int defaultType; private static int defaultPrec; private static int defaultScale; private static boolean defaultInPK; private static boolean defaultNullable; private static boolean defaultAutoInc; private static String defaultRemarks; private static String defaultForDefaultValue; private static String dbType; /** * referenceCount is meant to keep track of how many containers (i.e. * folders and relationships) have a reference to a column. A new * SQLColumn always starts off life with a reference count of 1. (it * is set in the constructors). * * When creating a new relationship which reuses a column, the * call addReference() on the column to increment the referenceCount. * * When removing a relationship, call removeReference() on the column to * decrement the referenceCount. If the referenceCount falls to zero, it * removes itself from the containing table (because it imported by * the creation of a relationship. */ protected int referenceCount; /** * Helper objects to resolve variables in check constraints. */ private final SQLCheckConstraintVariableResolver variableResolver = new SQLCheckConstraintVariableResolver(this); public SQLColumn() { userDefinedSQLType = new UserDefinedSQLType(); userDefinedSQLType.setParent(this); setPlatform(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM); logger.debug("NEW COLUMN (noargs) @"+hashCode()); logger.debug("SQLColumn() set ref count to 1"); referenceCount = 1; setName(defaultName); setPhysicalName(""); userDefinedSQLType.setType(defaultType); userDefinedSQLType.setPrecision(platform, defaultPrec); if (defaultPrec > 0) { userDefinedSQLType.setPrecisionType(platform, PropertyType.VARIABLE); } else { userDefinedSQLType.setPrecisionType(platform, PropertyType.NOT_APPLICABLE); } userDefinedSQLType.setScale(platform, defaultScale); if (defaultScale > 0) { userDefinedSQLType.setScaleType(platform, PropertyType.VARIABLE); } else { userDefinedSQLType.setScaleType(platform, PropertyType.NOT_APPLICABLE); } if (defaultNullable) { userDefinedSQLType.setMyNullability(DatabaseMetaData.columnNullable); } else { userDefinedSQLType.setMyNullability(DatabaseMetaData.columnNoNulls); } userDefinedSQLType.setMyAutoIncrement(defaultAutoInc); userDefinedSQLType.setDefaultValue(platform, defaultForDefaultValue); this.remarks = defaultRemarks; setPopulated(true); } public SQLColumn(UserDefinedSQLType startingType) { userDefinedSQLType = new UserDefinedSQLType(); userDefinedSQLType.setParent(this); userDefinedSQLType.setUpstreamType(startingType); setPlatform(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM); setName(defaultName); setPhysicalName(""); setRemarks(defaultRemarks); referenceCount = 1; setPopulated(true); } /** * A constructor for testing purposes, and reverse engineering. You normally do not want to call this * constructor because it will override all of your domain or type values. */ public SQLColumn(SQLTable parentTable, String colName, int dataType, String nativeType, int precision, int scale, int nullable, String remarks, String defaultValue, boolean isAutoIncrement) { if (parentTable != null) { logger.debug("NEW COLUMN "+colName+"@"+hashCode()+" parent "+parentTable.getName()+"@"+parentTable.hashCode()); } else { logger.debug("NEW COLUMN "+colName+"@"+hashCode()+" (null parent)"); } if (parentTable != null) { setParent(parentTable); } this.userDefinedSQLType = new UserDefinedSQLType(); userDefinedSQLType.setParent(this); setPlatform(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM); this.setName(colName); setPopulated(true); this.userDefinedSQLType.setType(dataType); // A scale/precision value of 0 does not mean anything, // which means the scale/precision type should be not applicable. // Otherwise, set the scale/precision type to a default of variable. // Reverse engineered tables will override the scale/precision types // by looking through existing user types and inheriting their // scale/precision properties. this.userDefinedSQLType.setScale(platform, scale); if (scale > 0) { this.userDefinedSQLType.setScaleType(platform, PropertyType.VARIABLE); } else { this.userDefinedSQLType.setScaleType(platform, PropertyType.NOT_APPLICABLE); } this.userDefinedSQLType.setPrecision(platform, precision); if (precision > 0) { this.userDefinedSQLType.setPrecisionType(platform, PropertyType.VARIABLE); } else { this.userDefinedSQLType.setPrecisionType(platform, PropertyType.NOT_APPLICABLE); } this.userDefinedSQLType.setMyNullability(nullable); this.userDefinedSQLType.setDefaultValue(platform, defaultValue); this.userDefinedSQLType.setMyAutoIncrement(isAutoIncrement); this.userDefinedSQLType.setName(nativeType); this.remarks = remarks; logger.debug("SQLColumn(.....) set ref count to 1"); this.referenceCount = 1; } /** * A constructor for testing purposes, and reverse engineering. You normally do not want to call this * constructor because it will override all of your domain or type values. */ public SQLColumn(SQLTable parentTable, String colName, UserDefinedSQLType type, int precision, int scale, boolean isAutoIncrement) { if (parentTable != null) { setParent(parentTable); } this.userDefinedSQLType = new UserDefinedSQLType("UserDefinedSQLType", DatabaseMetaData.columnNullable, isAutoIncrement, null, type, new SQLTypePhysicalProperties(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM)); userDefinedSQLType.setParent(this); setPlatform(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM); this.setName(colName); setPopulated(true); // A scale/precision value of 0 does not mean anything, // which means the scale/precision type should be not applicable. // Otherwise, set the scale/precision type to a default of variable. // Reverse engineered tables will override the scale/precision types // by looking through existing user types and inheriting their // scale/precision properties. this.userDefinedSQLType.setScale(platform, scale); if (scale > 0) { this.userDefinedSQLType.setScaleType(platform, PropertyType.VARIABLE); } else { this.userDefinedSQLType.setScaleType(platform, PropertyType.NOT_APPLICABLE); } this.userDefinedSQLType.setPrecision(platform, precision); if (precision > 0) { this.userDefinedSQLType.setPrecisionType(platform, PropertyType.VARIABLE); } else { this.userDefinedSQLType.setPrecisionType(platform, PropertyType.NOT_APPLICABLE); } this.userDefinedSQLType.setMyNullability(DatabaseMetaData.columnNullable); logger.debug("SQLColumn(.....) set ref count to 1"); this.referenceCount = 1; } /** * Constructs a SQLColumn that will be a part of the given SQLTable. * * @param parentTable The table that this column will think it belongs to. * @param colName This column's name. * @param nativeType The type as it is called in the source database. * @param scale The length of this column. Size is type-dependant. * @param precision The number of places of precision after the decimal place for numeric types. * @param nullable This column's nullability. One of: * <ul><li>DatabaseMetaData.columnNoNulls - might not allow NULL values * <li>DatabaseMetaData.columnNullable - definitely allows NULL values * <li>DatabaseMetaData.columnNullableUnknown - nullability unknown * </ul> * @param remarks User-defined remarks about this column * @param defaultValue The value this column will have if another value is not specified. * @param primaryKeySeq This column's position in the table's primary key. Null if it is not in the PK. * @param isAutoIncrement Does this column auto-increment? */ @Constructor public SQLColumn(@ConstructorParameter(propertyName = "parent") SQLTable parentTable, @ConstructorParameter(propertyName = "name") String colName, @ConstructorParameter(propertyName = "sourceDataTypeName") String nativeType, @ConstructorParameter(propertyName = "remarks") String remarks, @ConstructorParameter(parameterType=ParameterType.CHILD, propertyName="userDefinedSQLType") UserDefinedSQLType userDefinedSQLType) { if (parentTable != null) { logger.debug("NEW COLUMN "+colName+"@"+hashCode()+" parent "+parentTable.getName()+"@"+parentTable.hashCode()); } else { logger.debug("NEW COLUMN "+colName+"@"+hashCode()+" (null parent)"); } if (parentTable != null) { setParent(parentTable); } this.userDefinedSQLType = userDefinedSQLType; userDefinedSQLType.setParent(this); setPlatform(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM); this.setName(colName); setPopulated(true); this.remarks = remarks; logger.debug("SQLColumn(.....) set ref count to 1"); this.referenceCount = 1; } public SQLColumn(SQLTable parent, String colName, int type, int precision, int scale) { this(parent, colName, type, null, precision, scale, DatabaseMetaData.columnNullable, null, null, false); } /** * Creates a reasonable facsimile of the given column. */ public SQLColumn(SQLColumn col) { super(); userDefinedSQLType = new UserDefinedSQLType(); userDefinedSQLType.setParent(this); setPlatform(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM); setPopulated(true); copyProperties(this, col); logger.debug("SQLColumn(SQLColumn col ["+col+" "+col.hashCode()+"]) set ref count to 1"); referenceCount = 1; } /** * Makes a near clone of the given source column. The new column * you get back will have a parent pointer of addTo.columnsFolder, * but will not be attached as a child (you will normally do that * right after calling this). It will refer to this column as its * sourceColumn property if the source is in the same session, * and otherwise be identical to source. * * createInheritingInstance is used for reverse engineering. * Will not preserve listeners. * */ public SQLColumn createInheritingInstance(SQLTable addTo) { logger.debug("derived instance SQLColumn constructor invocation."); SQLColumn c = new SQLColumn(); copyProperties(c, this); c.setParent(addTo); if (SQLObjectUtils.isInSameSession(c, this)) { c.sourceColumn = this; } else { c.sourceColumn = null; } logger.debug("getDerivedInstance set ref count to 1"); c.referenceCount = 1; return c; } /** * Makes a copy of the given source column. The new column * you get back will have a parent pointer of addTo.columnsFolder, * but will not be attached as a child (you will normally do that * right after calling this). Listeners will not be added to this * copy. */ public SQLColumn createCopy(SQLTable addTo, boolean preserveSource) { logger.debug("derived instance SQLColumn constructor invocation."); SQLColumn c = new SQLColumn(); copyProperties(c, this); c.setParent(addTo); if (preserveSource) { c.sourceColumn = getSourceColumn(); } logger.debug("getDerivedInstance set ref count to 1"); c.referenceCount = 1; return c; } /** * Copies all the interesting properties of source into target. This is a subroutine of * the copy constructor, getDerivedInstance, and updateToMatch. * * @param target The instance to copy properties into. * @param source The instance to copy properties from. */ private static final void copyProperties(final SQLColumn target, final SQLColumn source) { target.runInForeground(new Runnable() { public void run() { target.begin("Copying SQLColumn."); target.setPlatform(source.getPlatform()); target.setName(source.getName()); target.setPhysicalName(source.getPhysicalName()); target.setRemarks(source.remarks); target.setAutoIncrementSequenceName(source.autoIncrementSequenceName); target.setEtlAuditable(source.getEtlAuditable()); target.setEtlDefaultValue(source.getEtlDefaultValue()); target.setEtlNotes(source.getEtlNotes()); target.setEtlTransformationLogic(source.getEtlTransformationLogic()); UserDefinedSQLType.copyProperties( target.getUserDefinedSQLType(), source.getUserDefinedSQLType()); target.commit(); } }); } /** * Updates all properties on this column (except the parent) to match the * given column's properties. * * @param source * The SQLColumn to copy the properties from */ @Override public void updateToMatch(SQLObject source) { copyProperties(this, (SQLColumn) source); } /** * Creates a list of unparented SQLColumn objects based on the current * information from the given DatabaseMetaData. * * @return A map of table names to a list of SQLColumns that the table * should contain in the order the columns should appear in the * table. This will contain the names and columns of tables that * are system tables which may not be interesting for all calling * methods. */ static ListMultimap<String, SQLColumn> fetchColumnsForTable( String catalog, String schema, String table, DatabaseMetaData dbmd) throws SQLException, DuplicateColumnException, SQLObjectException { ResultSet rs = null; final ListMultimap<String, SQLColumn> multimap = ArrayListMultimap.create(); try { logger.debug("SQLColumn.addColumnsToTables: catalog="+catalog+"; schema="+schema); rs = dbmd.getColumns(catalog, schema, table, "%"); int autoIncCol = SQL.findColumnIndex(rs, "is_autoincrement"); logger.debug("Auto-increment info column: " + autoIncCol); while (rs.next()) { logger.debug("addColumnsToTable SQLColumn constructor invocation."); String tableName = rs.getString(3); boolean autoIncrement; if (autoIncCol > 0) { autoIncrement = "yes".equalsIgnoreCase(rs.getString(autoIncCol)); } else { autoIncrement = false; } String nativeTypeName = rs.getString(6); if(nativeTypeName.indexOf('(') >= 0) { nativeTypeName = nativeTypeName.substring(0, nativeTypeName.indexOf('(')); } setDbType(dbmd.getDatabaseProductName()); SQLColumn col = new SQLColumn(null, rs.getString(4), // col name rs.getInt(5), // data type (from java.sql.Types) nativeTypeName, // native type name rs.getInt(7), // column size (precision) rs.getInt(9), // decimal size (scale) rs.getInt(11), // nullable rs.getString(12) == null ? "" : rs.getString(12), // remarks rs.getString(13), // default value autoIncrement // isAutoIncrement ); logger.debug("Precision for the column " + rs.getString(4) + " is " + rs.getInt(7)); // work around oracle 8i bug: when table names are long and similar, // getColumns() sometimes returns columns from multiple tables! // XXX: should be moved to the JDBC Wrapper for Oracle String dbTableName = rs.getString(3); if (dbTableName != null) { if (!dbTableName.equalsIgnoreCase(tableName)) { logger.warn("Got column "+col.getName()+" from "+dbTableName +" in metadata for "+tableName+"; not adding this column."); continue; } } else { logger.warn("Table name not specified in metadata. Continuing anyway..."); } logger.debug("Adding column "+col.getName()); multimap.put(dbTableName, col); } return multimap; } finally { try { if (rs != null) rs.close(); } catch (SQLException ex) { logger.error("Couldn't close result set", ex); } } } /** * Takes in information about the current UserDefinedSQLTypes and assigns an * upstream type to each of the columns. This is a many step process. Each * step falls through to the next one if it can't find a type. * <p> * 1. Search the types by type name. If there are any number of matches, use * one. We don't really care which. <br> * 2. Search the types by forward engineering name. This means what each * type would forward engineer to on the given platform. If there is exactly * one result, we're done. If there are multiple results, prompt the user. <br> * 3. Search the types by JDBC type code. Follow the same guidelines as step * 2. <br> * 4. If we are connected to an enterprise server, create a new type with * the same name and JDBC type as the reverse engineered column. <br> * 5. Assign the unknown type as the parent. This type is fairly clueless, * and won't forward engineer correctly on its own. * <p> * By the end of this process, all columns will have best match upstream * types which, barring some fiddling on the user's part (or the unknown * type coming into play), will forward engineer back to the type they came * from. They will also have appropriate values overridden from their new * upstream type. Any values that match up with defaults will not be * overridden, so that changing the type will change all of these columns. * * @param columns * A list of columns to assign types to. These should all be from * the same database platform. If any columns in the list already * have types assigned, they will be safely ignored. * @param dsCollection * The datasource collection that contains the user defined * types. * @param fromPlatform * The name of the platform these columns were reverse engineered * from, obtained using {@link JDBCDataSourceType#getName()}. * @param upf * A UserPrompterFactory, used for asking the user to * decide between multiple appropriate types. */ public static void assignTypes( List<SQLColumn> columns, DataSourceCollection<? extends SPDataSource> dsCollection, String fromPlatform, UserPrompterFactory upf) { if (fromPlatform == null) return; // Dropped from within the PlayPen List<UserDefinedSQLType> types = dsCollection.getSQLTypes(); Map<String, UserDefinedSQLType> typeMapByReverseName = new HashMap<String, UserDefinedSQLType>(); ListMultimap<String, UserDefinedSQLType> typeMapByForwardName = LinkedListMultimap.create(); ListMultimap<Integer, UserDefinedSQLType> typeMapByCode = LinkedListMultimap.create(); for (UserDefinedSQLType type : types) { typeMapByReverseName.put(type.getName().toLowerCase(), type); typeMapByForwardName.put(type.getPhysicalProperties(fromPlatform).getName().toLowerCase(), type); typeMapByCode.put(type.getType(), type); } Map<String, UserPrompter> userPrompters = new HashMap<String, UserPrompter>(); for (SQLColumn column : columns) { if (column.getUserDefinedSQLType().getUpstreamType() != null) continue; String nativeType = column.getSourceDataTypeName(); if (nativeType != null) { nativeType = nativeType.toLowerCase(); } logger.info("Column has type " + nativeType); UserDefinedSQLType upstreamType; if (typeMapByReverseName.containsKey(nativeType)) { upstreamType = typeMapByReverseName.get(nativeType); } else { List<UserDefinedSQLType> upstreamTypes; if (nativeType == null) { upstreamTypes = typeMapByForwardName.get(""); } else { upstreamTypes = typeMapByForwardName.get(nativeType.toLowerCase()); } if (upstreamTypes.size() == 0) { int jdbcCode = column.getType(); upstreamTypes = typeMapByCode.get(jdbcCode); if (upstreamTypes.size() == 0) { upstreamType = dsCollection.getNewSQLType(nativeType, jdbcCode); typeMapByForwardName.put(nativeType, upstreamType); typeMapByCode.put(jdbcCode, upstreamType); } else if (upstreamTypes.size() == 1) { upstreamType = upstreamTypes.get(0); } else { UserPrompter prompt; if (userPrompters.get(nativeType) != null) { prompt = userPrompters.get(nativeType); } else { prompt = upf.createListUserPrompter("Choose a type for " + column.getShortDisplayName(), upstreamTypes, upstreamTypes.get(0)); userPrompters.put(nativeType, prompt); } prompt.promptUser(); upstreamType = (UserDefinedSQLType) prompt.getUserSelectedResponse(); } } else if (upstreamTypes.size() == 1) { upstreamType = upstreamTypes.get(0); } else { UserPrompter prompt; if (userPrompters.containsKey(nativeType)) { prompt = userPrompters.get(nativeType); if(prompt instanceof ModalDialogListUserPrompter) { ((ModalDialogListUserPrompter<?>) prompt).setQuestionField("Choose a type for " + column.getShortDisplayName()); } } else { prompt = upf.createListUserPrompter("Choose a type for " + column.getShortDisplayName(), upstreamTypes, upstreamTypes.get(0)); userPrompters.put(nativeType, prompt); } prompt.promptUser(); upstreamType = (UserDefinedSQLType) prompt.getUserSelectedResponse(); } } UserDefinedSQLType type = column.getUserDefinedSQLType(); if (upstreamType != null) { type.setScaleType(fromPlatform, upstreamType.getScaleType(fromPlatform)); type.setPrecisionType(fromPlatform, upstreamType.getPrecisionType(fromPlatform)); if (upstreamType.getScale(fromPlatform) == type.getScale(fromPlatform) || upstreamType.getScaleType(fromPlatform) == PropertyType.NOT_APPLICABLE) { type.setScale(fromPlatform, null); } if (upstreamType.getPrecision(fromPlatform) == type.getPrecision(fromPlatform) || upstreamType.getPrecisionType(fromPlatform) == PropertyType.NOT_APPLICABLE) { type.setPrecision(fromPlatform, null); } if (upstreamType.getNullability() != null && upstreamType.getNullability().equals(type.getNullability())) { type.setMyNullability(null); } if (upstreamType.getDefaultValue(fromPlatform) != null && upstreamType.getDefaultValue(fromPlatform).equals(type.getDefaultValue(fromPlatform))) { type.setDefaultValue(fromPlatform, null); } if (upstreamType.getAutoIncrement() != null && upstreamType.getAutoIncrement().equals(type.getAutoIncrement())) { type.setMyAutoIncrement(null); } type.setUpstreamType(upstreamType); } } } /** * A comparator for SQLColumns that only pays attention to the * column names. For example, if <code>column1</code> has * <code>name = "MY_COLUMN"</code> and <code>type = * VARCHAR(20)</code> and <code>column2</code> has <code>name = * "MY_COLUMN"</code> and type <code>VARCHAR(4)</code>, * <code>compare(column1, column2)</code> will return 0. */ public static class ColumnNameComparator implements Comparator<SQLColumn> { /** * See class description for behaviour of this method. */ public int compare(SQLColumn c1, SQLColumn c2) { return c1.getName().compareTo(c2.getName()); } } public String toString() { return getShortDisplayName(); } // ------------------------- SQLObject support ------------------------- protected void populateImpl() throws SQLObjectException { // SQLColumn: populate is a no-op } @Transient @Accessor public boolean isPopulated() { return true; } /** * Returns a "formatted" version of the datatype including * any necessary parameters e.g. length for VARCHAR or precision and scale * for numeric datatypes */ @Transient @Accessor public String getTypeName() { // Type proxy should never override its upstream type's name. UserDefinedSQLType upstreamType = userDefinedSQLType.getUpstreamType(); StringBuilder name; String datatype; if (upstreamType == null) { if (userDefinedSQLType.getName() == null) { return ""; } else { name = new StringBuilder(userDefinedSQLType.getName()); datatype = userDefinedSQLType.getName(); } } else { name = new StringBuilder(upstreamType.getName()); datatype = upstreamType.getName(); } String defaultPlatform = SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM; int precision = userDefinedSQLType.getPrecision(defaultPlatform); int scale = userDefinedSQLType.getScale(defaultPlatform); PropertyType precisionType = userDefinedSQLType.getPrecisionType(defaultPlatform); PropertyType scaleType = userDefinedSQLType.getScaleType(defaultPlatform); if (precisionType != PropertyType.NOT_APPLICABLE && scaleType != PropertyType.NOT_APPLICABLE && precision > 0 && scale > 0) { name.append("(" + userDefinedSQLType.getPrecision(defaultPlatform) + ", " + userDefinedSQLType.getScale(defaultPlatform) + ")"); } else if (precisionType != PropertyType.NOT_APPLICABLE && precision > 0) { // In Microsoft SQL Server varchar(Max) limit is '2147483647', so its write it as varchar(2147483647) instead of varchar(Max) if ((datatype.equalsIgnoreCase("varchar") || datatype.equalsIgnoreCase("nvarchar")) && precision > 8000 && getDbType().equals("Microsoft SQL Server")) { name.append("(MAX)"); logger.debug("Precision changed for the column " + name + " from "+datatype+"(" + precision + ") to"+datatype+"(Max)"); } else { name.append("(" + userDefinedSQLType.getPrecision(defaultPlatform) + ")"); } } else if (scaleType != PropertyType.NOT_APPLICABLE && scale > 0) { name.append("(" + userDefinedSQLType.getScale(defaultPlatform) + ")"); } return name.toString(); } /** * Returns the column's name and the datatype * @see #getTypeName() * @see #getName() */ @Transient @Accessor public String getShortDisplayName() { return getName() + ": " + getTypeName(); } // ------------------------- accessors and mutators -------------------------- @Accessor(isInteresting=true) public String getPlatform() { return platform; } @Mutator public void setPlatform(String newPlatform) { String oldPlatform = platform; platform = newPlatform; begin("Setting platform"); firePropertyChange("platform", oldPlatform, newPlatform); if (userDefinedSQLType.getPhysicalProperties(newPlatform) == null) { userDefinedSQLType.putPhysicalProperties(newPlatform, new SQLTypePhysicalProperties(newPlatform)); } commit(); } @Accessor public UserDefinedSQLType getUserDefinedSQLType() { return userDefinedSQLType; } @NonBound public List<SQLCheckConstraint> getCheckConstraints() { return Collections.unmodifiableList( userDefinedSQLType.getCheckConstraints(getPlatform())); } public void addCheckConstraint(SQLCheckConstraint checkConstraint) { userDefinedSQLType.addCheckConstraint(getPlatform(), checkConstraint); } public boolean removeCheckConstraint(SQLCheckConstraint checkConstraint) { return userDefinedSQLType.removeCheckConstraint(getPlatform(), checkConstraint); } @Transient @Accessor(isInteresting = true) public SQLTypeConstraint getConstraintType() { return userDefinedSQLType.getConstraintType(getPlatform()); } @Transient @Mutator public void setConstraintType(SQLTypeConstraint constraint) { userDefinedSQLType.setConstraintType(getPlatform(), constraint); } @NonBound public List<SQLEnumeration> getEnumerations() { return userDefinedSQLType.getEnumerations(getPlatform()); } public void addEnumeration(SQLEnumeration enumeration) { userDefinedSQLType.addEnumeration(getPlatform(), enumeration); } public void removeEnumeration(SQLEnumeration enumeration) { userDefinedSQLType.removeEnumeration(getPlatform(), enumeration); } @Transient @Accessor(isInteresting = true) public PropertyType getPrecisionType() { return userDefinedSQLType.getPrecisionType(getPlatform()); } @Transient @Mutator public void setPrecisionType(PropertyType precisionType) { userDefinedSQLType.setPrecisionType(getPlatform(), precisionType); } @Transient @Accessor(isInteresting = true) public PropertyType getScaleType() { return userDefinedSQLType.getScaleType(getPlatform()); } @Transient @Mutator public void setScaleType(PropertyType scaleType) { userDefinedSQLType.setScaleType(getPlatform(), scaleType); } @Accessor public SQLColumn getSourceColumn() { return sourceColumn; } @Mutator public void setSourceColumn(SQLColumn col) { begin("Setting source column."); SQLColumn oldCol = this.sourceColumn; sourceColumn = col; firePropertyChange("sourceColumn",oldCol,col); commit(); } /** * Gets numeric value for the internal JDBC Type. * This value corresponds to the constants defined in java.sql.Types * * @return the value of type */ @Transient @Accessor(isInteresting = true) public int getType() { return userDefinedSQLType.getType(); } /** * Sets the value of type * * @param argType Value to assign to this.type */ @Transient @Mutator public void setType(UserDefinedSQLType type) { userDefinedSQLType.setUpstreamType(type); } /** * Sets the value of type * * @param argType Value to assign to this.type */ @Transient @Mutator public void setType(int type) { userDefinedSQLType.setType(type); } /** * The data type name as obtained during reverse engineering */ @Transient @Accessor public String getSourceDataTypeName() { return userDefinedSQLType.getName(); } @Transient @Mutator public void setSourceDataTypeName(String n) { userDefinedSQLType.setName(n); } /** * Gets the value of scale * * @return the value of scale */ @Transient @Accessor(isInteresting = true) public int getScale() { return userDefinedSQLType.getScale(getPlatform()); } /** * Sets the value of scale * * @param argScale Value to assign to this.scale */ @Transient @Mutator public void setScale(int argScale) { begin("Set scale"); userDefinedSQLType.setScale(getPlatform(), argScale); // Need to change scale type here according to the scale value // that is passed in. It is possible that when the project gets // loaded initially, the default SQLColumn() constructor gets called, // which sets the scale value and type to default. Since scale type // is not saved, only the scale value is set here. When getScale() // is called, it may return 0 because it will try to read the // scale type which could be NOT_APPLICABLE by default. if (argScale > 0 && getScaleType() == PropertyType.NOT_APPLICABLE) { setScaleType(PropertyType.VARIABLE); } else if (argScale == 0 && getScaleType() == PropertyType.VARIABLE) { setScaleType(null); } commit(); } /** * Returns the precision for this column. * The precision only makes sense together with the actual datatype. * For character datatypes this defines the max. length of the column. * For numeric datatypes this needs to be combined with the scale of the column * * @return the value of precision * @see #getScale() * @see #getType() * @see #getTypeName() */ @Transient @Accessor(isInteresting = true) public int getPrecision() { return userDefinedSQLType.getPrecision(getPlatform()); } /** * Sets the value of precision * * @param argPrecision Value to assign to this.precision */ @Transient @Mutator public void setPrecision(int argPrecision) { begin("Set precision"); userDefinedSQLType.setPrecision(getPlatform(), argPrecision); // Need to change precision type here according to the precision value // that is passed in. It is possible that when the project gets // loaded initially, the default SQLColumn() constructor gets called, // which sets the precision value and type to default. Since precision type // is not saved, only the precision value is set here. When getPrecision() // is called, it may return 0 because it will try to read the // precision type which could be NOT_APPLICABLE by default if (argPrecision > 0 && getPrecisionType() == PropertyType.NOT_APPLICABLE) { setPrecisionType(PropertyType.VARIABLE); } else if (argPrecision == 0 && getPrecisionType() == PropertyType.VARIABLE) { setPrecisionType(null); } commit(); } /** * Figures out this column's nullability * * @return true iff this.nullable == DatabaseMetaData.columnNullable. */ @NonBound public boolean isDefinitelyNullable() { return getNullable() == DatabaseMetaData.columnNullable; } /** * Returns true if this column is part of the table's primary key * * @return whether or not primaryKeySeq is defined */ @Transient @Accessor(isInteresting=true) public boolean isPrimaryKey() { return getParent().isInPrimaryKey(this); } /** * If this column is a foreign key, return the referenced table. * * @return null, if this column is not a FK column, the referenced table otherwise */ @NonBound public SQLTable getReferencedTable() { if (getParent() == null) return null; try { for (SQLImportedKey k : getParent().getImportedKeys()) { if (k.getRelationship().containsFkColumn(this)) { return k.getRelationship().getParent(); } } return null; } catch (SQLObjectException ex) { throw new SQLObjectRuntimeException(ex); } } /** * Indicates whether this column is a foreign key * * @return whether this column exists as a foreign key column in any of * its parent table's imported keys * * @see #getReferencedTable() */ @NonBound public boolean isForeignKey() { return getReferencedTable() != null; } /** * Indicates whether or not this column is exported into a foreign key in a * child table. * * @return Returns true if this column is in an exported key. Otherwise, * returns false. */ @NonBound public boolean isExported() { if (getParent() == null) return false; try { for (SQLRelationship r : getParent().getExportedKeys()) { if (r.containsPkColumn(this)) { return true; } } return false; } catch (SQLObjectException ex) { throw new SQLObjectRuntimeException(ex); } } /** * Returns whether this column is in an index */ @NonBound public boolean isIndexed() { if (getParent() == null) return false; try { for (SQLIndex ind : getParent().getIndices()) { for (SQLIndex.Column col : ind.getChildren(SQLIndex.Column.class)) { if (this.equals(col.getColumn())) { return true; } } } return false; } catch (SQLObjectException ex) { throw new SQLObjectRuntimeException(ex); } } /** * Returns whether this column is in an unique index. */ @NonBound public boolean isUniqueIndexed() { if (getParent() == null) return false; try { for (SQLIndex ind : getParent().getIndices()) { if (!ind.isUnique()) continue; for (SQLIndex.Column col : ind.getChildren(SQLIndex.Column.class)) { if (this.equals(col.getColumn())) { return true; } } } return false; } catch (SQLObjectException ex) { throw new SQLObjectRuntimeException(ex); } } /** * Returns the parent SQLTable object. */ @Accessor public SQLTable getParent() { return (SQLTable) super.getParent(); } /** * Because we constrained the return type on getParent there needs to be a * setter that has the same constraint otherwise the reflection in the undo * events will not find a setter to match the getter and won't be able to * undo parent property changes. */ @Mutator public void setParent(SQLTable parent) { super.setParent(parent); } /** * Returns this column's nullability. * * @return One of: * <ul><li>DatabaseMetaData.columnNoNulls - might not allow NULL values * <li>DatabaseMetaData.columnNullable - definitely allows NULL values * <li>DatabaseMetaData.columnNullableUnknown - nullability unknown * </ul> */ @Transient @Accessor(isInteresting=true) public int getNullable() { logger.debug(userDefinedSQLType); return userDefinedSQLType.getNullability(); } /** * Sets this column's nullability. * * @param argNullable One of: * <ul><li>DatabaseMetaData.columnNoNulls - might not allow NULL values * <li>DatabaseMetaData.columnNullable - definitely allows NULL values * <li>DatabaseMetaData.columnNullableUnknown - nullability unknown * </ul> */ @Transient @Mutator public void setNullable(int argNullable) { userDefinedSQLType.setMyNullability(argNullable); } public static String getDefaultName() { return defaultName; } public static void setDefaultName(String defaultName) { SQLColumn.defaultName = defaultName; } public static int getDefaultType() { return defaultType; } public static void setDefaultType(int defaultType) { SQLColumn.defaultType = defaultType; } public static int getDefaultPrec() { return defaultPrec; } public static void setDefaultPrec(int defaultPrec) { SQLColumn.defaultPrec = defaultPrec; } public static int getDefaultScale() { return defaultScale; } public static void setDefaultScale(int defaultScale) { SQLColumn.defaultScale = defaultScale; } public static boolean isDefaultInPK() { return defaultInPK; } public static void setDefaultInPK(boolean defaultInPK) { SQLColumn.defaultInPK = defaultInPK; } public static boolean isDefaultNullable() { return defaultNullable; } public static void setDefaultNullable(boolean defaultNullable) { SQLColumn.defaultNullable = defaultNullable; } public static boolean isDefaultAutoInc() { return defaultAutoInc; } public static void setDefaultAutoInc(boolean defaultAutoInc) { SQLColumn.defaultAutoInc = defaultAutoInc; } public static String getDefaultRemarks() { return defaultRemarks; } public static void setDefaultRemarks(String defaultRemarks) { SQLColumn.defaultRemarks = defaultRemarks; } public static String getDefaultForDefaultValue() { return defaultForDefaultValue; } public static void setDefaultForDefaultValue(String defaultForDefaultValue) { SQLColumn.defaultForDefaultValue = defaultForDefaultValue; } public static String getDbType() { return dbType; } public static void setDbType(String dbType) { SQLColumn.dbType = dbType; } /** * Get the comment/remark defined for this column. * * @return the value of remarks */ @Accessor(isInteresting=true) public String getRemarks() { return this.remarks; } /** * Sets the value of remarks * * @param argRemarks Value to assign to this.remarks */ @Mutator public void setRemarks(String argRemarks) { String oldRemarks = this.remarks; this.remarks = argRemarks; firePropertyChange("remarks",oldRemarks,argRemarks); } /** * Returns the default value defined for this value * * @return the value of defaultValue */ @NonProperty public String getDefaultValue() { return userDefinedSQLType.getDefaultValue(getPlatform()); } /** * Sets the value of defaultValue * * @param argDefaultValue Value to assign to this.defaultValue */ @NonProperty public void setDefaultValue(String argDefaultValue) { userDefinedSQLType.setDefaultValue(getPlatform(), argDefaultValue); } /** * Gets the value of autoIncrement * * @return the value of autoIncrement */ @Transient @Accessor(isInteresting=true) public boolean isAutoIncrement() { return userDefinedSQLType.getAutoIncrement(); } /** * Sets the value of autoIncrement * * @param argAutoIncrement Value to assign to this.autoIncrement */ @Transient @Mutator public void setAutoIncrement(boolean argAutoIncrement) { userDefinedSQLType.setMyAutoIncrement(argAutoIncrement); } /** * Returns the auto-increment sequence name, or a made-up * default (<code><i>parentTableName</i>_<i>columnName</i>_seq</code>) if the sequence name * has not been set explicitly. The auto-increment sequence * name is a hint to DDL generators for platforms that need * sequence objects to support auto-incrementing column values. */ @Accessor public String getAutoIncrementSequenceName() { if (autoIncrementSequenceName == null) { return makeAutoIncrementSequenceName(); } else { return autoIncrementSequenceName; } } /** * Creates an auto-increment sequence name based on table and column names. * * @return The properly formatted sequence name for the column. */ public String makeAutoIncrementSequenceName() { String tableName; if (getParent() == null) { tableName = ""; } else if (getParent().getPhysicalName() != null && !getPhysicalName().trim().equals("")) { tableName = getParent().getPhysicalName() + "_"; } else { tableName = getParent().getName() +"_"; } return tableName + getPhysicalName() + "_seq"; } public String discoverSequenceNameFormat(String tableName, String colName) { String seqName = getAutoIncrementSequenceName(); String seqNamePrefix, seqNameSuffix; String newName = ""; int prefixEnd = seqName.indexOf(colName); if ((prefixEnd != -1 && seqName.substring (prefixEnd + colName.length()).indexOf(colName) == -1)) { seqNamePrefix = seqName.substring(0, prefixEnd); seqNameSuffix = seqName.substring(prefixEnd + colName.length()); } else if (seqName.equals(tableName + "_" + colName + "_seq")) { seqNamePrefix = tableName + "_"; seqNameSuffix = "_seq"; } else { seqNamePrefix = null; seqNameSuffix = null; } if (seqNamePrefix != null && seqNameSuffix != null) { newName = seqNamePrefix + colName + seqNameSuffix; } return newName; } /** * Only sets the name if it is different from the default name. This is important * in case the table name changes; the name should be expected to update. */ @Mutator public void setAutoIncrementSequenceName(String autoIncrementSequenceName) { // have to use getter because it produces the default value String oldName = getAutoIncrementSequenceName(); if (!oldName.equals(autoIncrementSequenceName)) { begin("Setting autoIncrementSequenceName."); this.autoIncrementSequenceName = autoIncrementSequenceName; firePropertyChange("autoIncrementSequenceName", oldName, autoIncrementSequenceName); commit(); } } /** * Returns true if the auto-increment sequence name of this column has * been changed from its default value. Code that loads and saves this * SQLColumn will want to know if the value is a default or not. */ @NonProperty public boolean isAutoIncrementSequenceNameSet() { return autoIncrementSequenceName != null; } public void addReference() { int oldReference = referenceCount; referenceCount++; logger.debug("incremented reference count to: " + referenceCount); firePropertyChange("referenceCount", oldReference, referenceCount); } public void removeReference() { if (logger.isDebugEnabled()) { String parentName = "<no parent table>"; if (getParent() != null && getParent() != null) { parentName = getParent().getName(); } logger.debug("Trying to remove reference from "+parentName+"."+getName()+" "+hashCode()); } if (referenceCount == 0) { logger.debug("Reference count of "+ this.getParent() +"."+this+" was already 0"); throw new IllegalStateException("Reference count of is already 0; can't remove any references!"); } int oldReference = referenceCount; referenceCount--; logger.debug("decremented reference count to: " + referenceCount); if (referenceCount == 0) { // delete from the parent (columnsFolder) if (getParent() != null){ logger.debug("reference count is 0, deleting column from parent."); try { getParent().removeChild(this); } catch (IllegalArgumentException e) { throw new RuntimeException(e); } catch (ObjectDependentException e) { throw new RuntimeException(e); } } else { logger.debug("Already removed from parent"); } } firePropertyChange("referenceCount", oldReference, referenceCount); } /** * @return Returns the referenceCount. */ @Transient @Accessor public int getReferenceCount() { return referenceCount; } /** * WARNING this should not be used by hand coded objects * @param referenceCount * @deprecated This method exists only to be called reflectively by the undo manager. You should use addReference() and removeReference(). */ @Deprecated @NonBound public void setReferenceCount(int referenceCount) { this.referenceCount = referenceCount; } @Override @NonProperty public List<? extends SQLObject> getChildrenWithoutPopulating() { return Collections.singletonList(userDefinedSQLType); } @Override protected boolean removeChildImpl(SPObject child) { return false; } @NonProperty public List<? extends SPObject> getDependencies() { return Collections.emptyList(); } public void removeDependency(SPObject dependency) { // no-op } @NonProperty public List<Class<? extends SPObject>> getAllowedChildTypes() { return allowedChildTypes; } @NonProperty public SPVariableResolver getVariableResolver() { return variableResolver; } public void addCheckConstraint(SQLCheckConstraint checkConstraint, int index) { userDefinedSQLType.addCheckConstraint(getPlatform(), checkConstraint, index); } @Override @Mutator public void setName(String name) { try { begin("Setting name and possibly physical or primary key name."); String oldName = getName(); super.setName(name); if (isMagicEnabled()) { updatePhysicalNameToMatch(oldName, name); } commit(); } catch (Throwable t) { rollback(t.getMessage()); throw new RuntimeException(t); } } /** * Returns the ETL transformation logic used to get from the source column * to this target column. Currently, this property is only used in SQL Power * Architect Enterprise Edition. */ @Accessor(isInteresting=true) public String getEtlTransformationLogic() { return etlTransformationLogic; } /** * Sets the ETL transformation logic used to get from the source column to * this target column. Currently, this property is only used in SQL Power * Architect Enterprise Edition. * * @param etlTransformationLogic * The ETL transformation logic. */ @Mutator public void setEtlTransformationLogic(String etlTransformationLogic) { String oldTransformationLogic = this.etlTransformationLogic; this.etlTransformationLogic = etlTransformationLogic; firePropertyChange("etlTransformationLogic", oldTransformationLogic, etlTransformationLogic); } /** * Gets the ETL mapping default value. This column should be set to this * default value during an ETL transformation process. * * @return The ETL mapping default value. */ @Accessor(isInteresting=true) public String getEtlDefaultValue() { return etlDefaultValue; } /** * Sets the ETL mapping default value. This column should be set to this * default value during an ETL transformation process. * * @param etlDefaultValue * The ETL mapping default value. */ @Mutator public void setEtlDefaultValue(String etlDefaultValue) { String oldDefaultValue = this.etlDefaultValue; this.etlDefaultValue = etlDefaultValue; firePropertyChange("etlDefaultValue", oldDefaultValue, etlDefaultValue); } /** * Returns a {@link Boolean} that determines if the ETL mapping from this * target column to the source column is auditable. Auditable means that a * comparison should be done outside the ETL after the process has been * running for a long period of time (e.g. 1 month). Currently, this * property is only used in SQL Power Architect Enterprise Edition. * * If true is returned, the ETL mapping is auditable. If null is returned, * there is no ETL mapping. */ @Accessor(isInteresting=true) public Boolean getEtlAuditable() { return etlAuditable; } /** * Sets a {@link Boolean} that determines if the ETL mapping from this * target column to the source column is auditable. Currently, this property * is only used in SQL Power Architect Enterprise Edition. * * @param etlAuditable * The {@link Boolean} auditable value. If true, then the ETL * mapping is auditable. This value can be null, which indicates * that there is no ETL mapping. */ @Mutator public void setEtlAuditable(Boolean etlAuditable) { Boolean oldAuditable = this.etlAuditable; this.etlAuditable = etlAuditable; firePropertyChange("etlAuditable", oldAuditable, etlAuditable); } /** * Returns a {@link String} description about the ETL mapping from this * column to the source column, extra to the transformation logic. * Currently, this property is only used in SQL Power Architect Enterprise * Edition. */ @Accessor(isInteresting=true) public String getEtlNotes() { return etlNotes; } /** * Sets a description regarding anything about the ETL mapping from this * column to this source column that is not already defined in the ETL * transformation logic. Currently, this property is only used in SQL Power * Architect Enterprise Edition. * * @param etlNotes * The description about the ETL mapping. */ @Mutator public void setEtlNotes(String etlNotes) { String oldNotes = this.etlNotes; this.etlNotes = etlNotes; firePropertyChange("etlNotes", oldNotes, etlNotes); } }