/*
* 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.beans.PropertyChangeEvent;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.log4j.Logger;
import ca.sqlpower.object.AbstractSPListener;
import ca.sqlpower.object.ObjectDependentException;
import ca.sqlpower.object.SPChildEvent;
import ca.sqlpower.object.SPListener;
import ca.sqlpower.object.SPObject;
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.NonProperty;
import ca.sqlpower.object.annotation.Transient;
import ca.sqlpower.sql.SQL;
import ca.sqlpower.util.SessionNotFoundException;
import ca.sqlpower.util.TransactionEvent;
/**
* The SQLIndex class represents an index on a table in a relational database.
*
* @author fuerth
*/
public class SQLIndex extends SQLObject {
private static final Logger logger = Logger.getLogger(SQLIndex.class);
/**
* Defines an absolute ordering of the child types of this class.
*/
public static final List<Class<? extends SPObject>> allowedChildTypes =
Collections.<Class<? extends SPObject>>singletonList(Column.class);
/**
* An enumeration to define if a column in an index should be ordered in ascending
* order, descending order, or it should be left undefined.
*/
public static enum AscendDescend {
ASCENDING, DESCENDING, UNSPECIFIED;
}
/**
* This is the property name in the PL.ini file that will indicate what Index types
* are supported for any specific database.
*/
public static final String INDEX_TYPE_DESCRIPTOR = SQLIndex.class.getName() + ".IndexType";
/**
* This is the index type. If this is null the default type in the database should be used.
*/
private String type;
/**
* This is the name of the column that will be augmented by the custom
* JDBC wrappers to represent index type;
*/
public static final String RS_INDEX_TYPE_COL = "SPG_INDEX_TYPE";
/**
* A simple placeholder for a column. We're not using real SQLColumn instances here so that the
* tree of SQLObjects can remain tree-like. If we put the real SQLColumns in here, the columns
* would appear in two places in the tree (here and under the table's columns folder)!
*/
public static class Column extends SQLObject {
/**
* Defines an absolute ordering of the child types of this class.
*/
public static final List<Class<? extends SPObject>> allowedChildTypes = Collections.emptyList();
/**
* Small class for reacting to changes in this index columns's
* target SQLColumn (if it has one at all).
*/
private class TargetColumnListener implements SPListener {
/**
* Updates the index column name to match the new value in this
* event, if the event is a name change from the target SQLColumn.
* The process of doing the update will cause the SQLIndex.Column
* object to fire an event of its own.
*/
public void propertyChanged(PropertyChangeEvent e) {
if ("name".equals(e.getPropertyName())) {
setName((String) e.getNewValue());
} else if ("physicalName".equals(e.getPropertyName())) {
setPhysicalName((String) e.getNewValue());
}
}
public void childAdded(SPChildEvent e) {
// no-op
}
public void childRemoved(SPChildEvent e) {
// no-op
}
public void transactionStarted(TransactionEvent e) {
// no-op
}
public void transactionEnded(TransactionEvent e) {
// no-op
}
public void transactionRollback(TransactionEvent e) {
// no-op
}
@Override
public String toString() {
StringBuffer buf = new StringBuffer();
buf.append(getParent().getName());
buf.append(".");
buf.append(Column.this.getName());
buf.append(".");
buf.append("TargetColumnListener");
buf.append(" isPrimarykey?");
buf.append(getParent().isPrimaryKeyIndex());
return buf.toString();
}
}
/**
* The column in the table that this index column represents. Might be
* null if this index column represents an expression rather than a
* single column value.
*/
private SQLColumn column;
/**
* Specifies if the column is ascending, descending, or undefined.
*/
private AscendDescend ascendingOrDescending;
/**
* A proxy that refires certain events on the target column.
*
* <p>It is the job of {@link #setColumn(SQLColumn)} to keep this
* listener hooked up to the correct SQLColumn object (or completely
* disconnected in the case that there is no target SQLColumn).
*/
private final TargetColumnListener targetColumnListener = new TargetColumnListener();
/**
* Creates a Column object that corresponds to a particular SQLColumn.
*/
public Column(SQLColumn col, AscendDescend ad) {
this(col.getName(), ad);
setColumn(col);
}
/**
* Creates a Column object that does not correspond to a particular column
* (such as an expression index).
*/
public Column(
String name,
AscendDescend ad) {
setName(name);
setPopulated(true);
ascendingOrDescending = ad;
}
/**
* Creates a Column object that may or may not correspond to a column.
*
* @param name
* The name of the column being wrapped.
* @param col
* If this column is representing a {@link SQLColumn} the
* column must be passed in. If the column is representing an
* expression or a column other than a SQLColumn null must be
* passed in.
* @param ad
* Decides if the column should be indexed in ascending or
* descending order.
*/
@Constructor
public Column(@ConstructorParameter(propertyName="name") String name,
@ConstructorParameter(propertyName="column") SQLColumn col,
@ConstructorParameter(propertyName="ascendingOrDescending") AscendDescend ad) {
this(name, ad);
setColumn(col);
}
public Column() {
this((String) null, AscendDescend.UNSPECIFIED);
}
@Override
@Accessor
public SQLIndex getParent() {
return (SQLIndex) 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(SQLIndex parent) {
super.setParent(parent);
}
@Override
@Transient @Accessor
public String getShortDisplayName() {
return getName();
}
@Override
protected void populateImpl() throws SQLObjectException {
// nothing to do
}
@Override
@Transient @Accessor
public boolean isPopulated() {
return true;
}
/**
* NOTE: This column can be null if the column it represents is an expression
* and not a basic column.
*/
@Accessor
public SQLColumn getColumn() {
return column;
}
@Mutator
public void setColumn(SQLColumn column) {
try {
begin("Setting SQLIndex column");
if (this.column != null) {
this.column.removeSPListener(targetColumnListener);
}
SQLColumn oldValue = this.column;
this.column = column;
if (this.column != null) {
this.column.addSPListener(targetColumnListener);
}
firePropertyChange("column", oldValue, column);
if (this.column != null) {
setPhysicalName(column.getPhysicalName());
}
commit();
} catch (RuntimeException e) {
rollback(e.getMessage());
throw e;
}
}
@Accessor
public AscendDescend getAscendingOrDescending() {
return ascendingOrDescending;
}
/**
* This setter should be passed an enumerated item of type
* AscendDescend.
*/
@Mutator
public void setAscendingOrDescending(AscendDescend ad) {
AscendDescend oldValue = ascendingOrDescending;
ascendingOrDescending = (AscendDescend) ad;
firePropertyChange("ascendingOrDescending", oldValue, ascendingOrDescending);
}
@Mutator
public void setAscending(boolean ascending) {
AscendDescend oldValue = this.ascendingOrDescending;
if (ascending) {
this.ascendingOrDescending = AscendDescend.ASCENDING;
}
firePropertyChange("ascending", oldValue, ascendingOrDescending);
}
@Mutator
public void setDescending(boolean descending) {
AscendDescend oldValue = this.ascendingOrDescending;
if (descending) {
this.ascendingOrDescending = AscendDescend.DESCENDING;
}
firePropertyChange("descending", oldValue, descending);
}
@Override
public String toString() {
return getName();
}
@Override
public int hashCode() {
final int PRIME = 31;
int result = 1;
result = PRIME * result + (ascendingOrDescending == AscendDescend.ASCENDING ? 1231 : 1237);
result = PRIME * result + ((column == null) ? 0 : column.hashCode());
result = PRIME * result + (ascendingOrDescending == AscendDescend.DESCENDING ? 1231 : 1237);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Column other = (Column) obj;
if (ascendingOrDescending != other.ascendingOrDescending)
return false;
if (column == null) {
if (other.column != null)
return false;
} else if (!column.equals(other.column))
return false;
return true;
}
@Override
public List<? extends SQLObject> getChildrenWithoutPopulating() {
return Collections.emptyList();
}
@Override
protected boolean removeChildImpl(SPObject child) {
return false;
}
public List<? extends SPObject> getDependencies() {
return Collections.singletonList(column);
}
public void removeDependency(SPObject dependency) {
if (dependency == column) {
getParent().removeColumn(this);
}
}
public List<Class<? extends SPObject>> getAllowedChildTypes() {
return allowedChildTypes;
}
@Override
public void updateToMatch(SQLObject source) throws SQLObjectException {
Column sourceCol = (Column) source;
setAscendingOrDescending(sourceCol.getAscendingOrDescending());
}
}
/**
* Flags whether or not this index enforces uniqueness.
*/
private boolean unique;
/**
* The qualifier that must be used for referring to this index in the database. This is
* usually the name of the table the index belongs to (in the case of SQL Server), or null
* (in the case of Oracle).
*/
private String qualifier;
/**
* The filter condition on this index, if any. According to the ODBC programmer's reference,
* this is probably a property of the index as a whole (as opposed to the individual index columns),
* but it doesn't say that explicitly. According to the JDBC spec, this could be anything at all.
*/
private String filterCondition;
/**
* This indicates if an index is clustered or not.
*/
private boolean clustered;
/**
* This is a listener that will listen for SQLColumns in the SQLTable's column folder
* and make sure that the SQLIndex will also remove its Column object associated
* with the SQLColumn removed.
*/
private SPListener removeColumnListener;
private List<Column> columns = new ArrayList<Column>();
@Constructor
public SQLIndex(
@ConstructorParameter(propertyName = "name") String name,
@ConstructorParameter(propertyName = "unique") boolean unique,
@ConstructorParameter(propertyName = "qualifier") String qualifier,
@ConstructorParameter(propertyName = "type") String type,
@ConstructorParameter(propertyName = "filterCondition") String filter) {
this();
setName(name);
this.unique = unique;
this.qualifier = qualifier;
this.type = type;
this.filterCondition = filter;
}
public SQLIndex() {
super();
setPopulated(true);
removeColumnListener = new AbstractSPListener() {
public void childRemoved(SPChildEvent e) {
if (e.getChildType() == SQLColumn.class) {
removeColumnFromIndices(e);
}
}
};
}
/**
* Copy constructor for a sql index
* @param oldIndex
* @throws SQLObjectException
*/
public SQLIndex(SQLIndex oldIndex) throws SQLObjectException {
this();
setParent(oldIndex.getParent());
updateToMatch(oldIndex);
}
/**
* Updates all properties and child objects of this index to match the given
* index, except the parent pointer.
*
* @param source
* The index to copy properties and columns from. If it has columns,
* they must already belong to the same table as this index does.
*/
@Override
public final void updateToMatch(SQLObject source) throws SQLObjectException {
updateToMatch(source, true);
}
/**
* Updates all properties and child objects of this index to match the given
* index, except the parent pointer.
*
* @param source
* The index to copy properties and columns from. If it has
* columns, they must already belong to the same table as this
* index does.
* @param updateChildren
* If true the children of the index will be updated as well. If
* false only this index's parameters will be updated.
*/
public final void updateToMatch(SQLObject source, boolean updateChildren) throws SQLObjectException {
SQLIndex sourceIdx = (SQLIndex) source;
try {
begin("Updating SQLIndex to match source object.");
setName(sourceIdx.getName());
setUnique(sourceIdx.unique);
populated = sourceIdx.populated;
setType(sourceIdx.type);
setFilterCondition(sourceIdx.filterCondition);
setQualifier(sourceIdx.qualifier);
setClustered(sourceIdx.clustered);
setPhysicalName(sourceIdx.getPhysicalName());
if (updateChildren) {
makeColumnsLike(sourceIdx);
}
commit();
} catch (IllegalArgumentException e) {
rollback("Could not remove columns from SQLIndex: " + e.getMessage());
throw new RuntimeException(e);
} catch (ObjectDependentException e) {
rollback("Could not remove columns from SQLIndex: " + e.getMessage());
throw new RuntimeException(e);
}
}
/**
* Indices are associated with one or more table columns. The children of this index represent those columns,
* and the order in which the index applies to them.
*/
@Override
public boolean allowsChildren() {
return true;
}
/**
* Overriden to narrow return type.
*/
@Override
@NonProperty
public Column getChild(int index) throws SQLObjectException {
return (Column) super.getChild(index);
}
/**
* Overriden to narrow return type.
*/
@Override
@NonProperty
public List<Column> getChildrenWithoutPopulating() {
return Collections.unmodifiableList(new ArrayList<Column>(columns));
}
/**
* Returns the table folder that owns this index.
*/
@Override
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) {
setParentHelper(parent);
}
@Override
@Transient @Accessor
public String getShortDisplayName() {
return getName();
}
/**
* Indices are populated when first created, so populate is a no-op.
*/
@Override
protected void populateImpl() throws SQLObjectException {
// nothing to do
}
@Override
@Transient @Accessor
public boolean isPopulated() {
return true;
}
/**
* Updates this index's parent reference, and attaches a listener to the
* columns folder of the new parent's parent table.
*
* @param parent
* The new parent. Must be null or a SQLTable.Folder instance. If
* it's a folder, it must already have a parent table.
* @throws IllegalStateException
* if the given parent is non-null and does not itself have a
* parent table.
*/
@Override
@Mutator
public void setParent(SPObject parent) {
setParentHelper(parent);
}
/**
* See the documentation on {@link #setParent(SQLTable)} for why
* setting the parent seems kind of goofy.
*/
private void setParentHelper(SPObject parent) {
if (getParent() != null) {
getParent().removeSPListener(removeColumnListener);
}
super.setParent(parent);
if (getParent() != null) {
getParent().addSPListener(removeColumnListener);
}
}
/**
* This is used by the removeColumn method to make sure that once a column
* is removed from a table, it is also removed from all the indices of that table.
*/
private void removeColumnFromIndices(SPChildEvent e) {
if (getParent() != null && getParent().isMagicEnabled()) {
try {
//begin and commit on the parent table in case the index is removed from the system
//as the commit would then not be fired to the undo listeners.
SQLTable parentTable = getParent();
parentTable.begin("Removing column from indices");
for (int j = this.getChildCount() - 1; j >= 0; j--) {
Column col = getChild(j);
if (col.getColumn() != null && col.getColumn().equals(e.getChild())) {
removeChild(col);
}
}
cleanUpIfChildless();
parentTable.commit();
} catch (SQLObjectException e1) {
rollback("Could not remove child: " + e1.getMessage());
throw new SQLObjectRuntimeException(e1);
} catch (IllegalArgumentException e1) {
rollback("Could not remove child: " + e1.getMessage());
throw new RuntimeException(e1);
} catch (ObjectDependentException e1) {
rollback("Could not remove child: " + e1.getMessage());
throw new RuntimeException(e1);
}
}
}
/**
* This method is used to clean up the index when it no longer has any children.
*/
public void cleanUpIfChildless() {
//This is a final field on the table
if (isPrimaryKeyIndex()) return;
try {
if (getChildCount() == 0 && getParent() != null) {
logger.debug("Removing " + getName() + " index from table " + getParent().getName());
getParent().removeSPListener(removeColumnListener);
getParent().removeChild(this);
}
} catch (SQLObjectException e) {
throw new SQLObjectRuntimeException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (ObjectDependentException e) {
throw new RuntimeException(e);
}
}
@Override
protected void addChildImpl(SPObject child, int index) {
if (child instanceof SQLIndex.Column) {
Column c = (Column) child;
if (isPrimaryKeyIndex() && c.getColumn() == null) {
throw new IllegalArgumentException("The primary key index must consist of real columns, not expressions");
}
addIndexColumn(c, index);
if (c.getColumn() != null) {
// this will be redundant in some cases, but the addSQLObjectListener method
// checks for adding duplicate listeners and does nothing in that case
c.getColumn().addSPListener(c.targetColumnListener);
}
} else {
throw new IllegalArgumentException("The child " + child.getName() +
" of type " + child.getClass() + " is not a valid child type of " +
getClass() + ".");
}
}
@Accessor(isInteresting=true)
public String getFilterCondition() {
return filterCondition;
}
@Mutator
public void setFilterCondition(String filterCondition) {
String oldValue = this.filterCondition;
this.filterCondition = filterCondition;
firePropertyChange("filterCondition", oldValue, filterCondition);
}
@Accessor(isInteresting=true)
public String getQualifier() {
return qualifier;
}
@Mutator
public void setQualifier(String qualifier) {
String oldValue = this.qualifier;
this.qualifier = qualifier;
firePropertyChange("qualifier", oldValue, qualifier);
}
@Accessor(isInteresting=true)
public String getType() {
return type;
}
@Mutator
public void setType(String type) {
String oldValue = this.type;
this.type = type;
firePropertyChange("type", oldValue, type);
}
@Accessor(isInteresting=true)
public boolean isUnique() {
return unique;
}
@Accessor(isInteresting=true)
public boolean isClustered() {
return clustered;
}
@Mutator
public void setUnique(boolean unique) {
boolean oldValue = this.unique;
this.unique = unique;
firePropertyChange("unique", oldValue, unique);
}
@Mutator
public void setClustered(boolean value) {
boolean oldValue = this.clustered;
this.clustered = value;
firePropertyChange("clustered", oldValue, clustered);
}
/**
* Creates a list of new SQLIndex objects based on the indexes that exist on
* the given table in the given database metadata. The index child objects
* will have direct references to the columns of the given SQLTable, but
* nothing in the table will be modified. You can add the indexes to the
* table yourself when the list is returned.
* <p>
* As a side effect of calling this method the primary key of the table will
* be updated to match the primary key of the table in the database.
* <p>
* Note: Because the columns are added to the table in the foreground thread
* and this method can be called by a background thread you cannot rely on
* the columns to already exist while running on the background. If you push
* {@link Runnable}s to the foreground that need the children the children
* will exist by the time the foreground of the index is reached as the EDT
* is a queue.
*
* @param dbmd
* The metadata to read the index descriptions from
* @param targetTable
* The table the indexes are on.
*/
static List<SQLIndex> fetchIndicesForTableAndUpdatePK(DatabaseMetaData dbmd, final SQLTable targetTable) throws SQLException,
SQLObjectException {
ResultSet rs = null;
String catalog = targetTable.getCatalogName();
String schema = targetTable.getSchemaName();
String tableName = targetTable.getName();
List<SQLIndex> indexes = new ArrayList<SQLIndex>();
try {
String pkName = null;
rs = dbmd.getPrimaryKeys(catalog, schema, tableName);
final SortedMap<Integer, String> pkColPositionToName = new TreeMap<Integer, String>();
while (rs.next()) {
pkColPositionToName.put(rs.getInt(5) - 1, rs.getString(4));
String pkNameCheck = rs.getString(6);
if (pkName == null) {
pkName = pkNameCheck;
} else if (!pkName.equals(pkNameCheck)) {
throw new IllegalStateException(
"The PK name has changed from " + pkName + " to " +
pkNameCheck + " while adding indices to table");
}
}
Runnable runner = new Runnable() {
public void run() {
for (Map.Entry<Integer, String> namedPositions : pkColPositionToName.entrySet()) {
if (!targetTable.isColumnsPopulated()) {
throw new IllegalStateException("Table " + targetTable + " is missing columns, cannot populate primary key.");
}
try {
SQLColumn col = targetTable.getColumnByName(namedPositions.getValue(), false, true);
if (col != null) {
targetTable.changeColumnIndex(
targetTable.getColumnsWithoutPopulating().indexOf(col),
namedPositions.getKey(), true);
} else {
logger.error("Column " + namedPositions.getValue() + " not found in " + targetTable);
throw new RuntimeException("Column " + col.getName() + " not found in " + targetTable);
}
} catch (SQLObjectException e) {
throw new SQLObjectRuntimeException(e);
}
}
}
};
try {
targetTable.getRunnableDispatcher().runInForeground(runner);
} catch (SessionNotFoundException e) {
runner.run();
}
rs.close();
rs = null;
logger.debug("SQLIndex.addIndicesToTable: catalog=" + catalog + "; schema=" + schema + "; tableName=" +
tableName + "; primary key name=" + pkName);
SQLIndex idx = null;
rs = dbmd.getIndexInfo(catalog, schema, tableName, false, true);
while (rs.next()) {
/*
* DatabaseMetadata result set columns:
*
1 TABLE_CAT String => table catalog (may be null)
2 TABLE_SCHEM String => table schema (may be null)
3 TABLE_NAME String => table name
4 NON_UNIQUE boolean => Can index values be non-unique. false when TYPE is tableIndexStatistic
5 INDEX_QUALIFIER String => index catalog (may be null); null when TYPE is tableIndexStatistic
6 INDEX_NAME String => index name; null when TYPE is tableIndexStatistic
7 TYPE short => index type:
tableIndexStatistic - this identifies table statistics that are returned in conjuction with a table's index descriptions
tableIndexClustered - this is a clustered index
tableIndexHashed - this is a hashed index
tableIndexOther - this is some other style of index
8 ORDINAL_POSITION short => column sequence number within index; zero when TYPE is tableIndexStatistic
9 COLUMN_NAME String => column name; null when TYPE is tableIndexStatistic
10 ASC_OR_DESC String => column sort sequence, "A" => ascending, "D" => descending, may be null if sort sequence is not supported; null when TYPE is tableIndexStatistic
11 CARDINALITY int => When TYPE is tableIndexStatistic, then this is the number of rows in the table; otherwise, it is the number of unique values in the index.
12 PAGES int => When TYPE is tableIndexStatisic then this is the number of pages used for the table, otherwise it is the number of pages used for the current index.
13 FILTER_CONDITION String => Filter condition, if any. (may be null)
*/
boolean nonUnique = rs.getBoolean(4);
boolean isClustered = rs.getShort(7) == DatabaseMetaData.tableIndexClustered ? true : false;
String qualifier = rs.getString(5);
String name = rs.getString(6);
String type = null;
if (SQL.findColumnIndex(rs, RS_INDEX_TYPE_COL) > 0) {
type = rs.getString(RS_INDEX_TYPE_COL);
}
int pos = rs.getInt(8);
final String colName = rs.getString(9);
String ascDesc = rs.getString(10);
final AscendDescend aOrD;
if (ascDesc != null && ascDesc.equals("A")) {
aOrD = AscendDescend.ASCENDING;
} else if (ascDesc != null && ascDesc.equals("D")) {
aOrD = AscendDescend.DESCENDING;
} else {
aOrD = AscendDescend.UNSPECIFIED;
}
String filter = rs.getString(13);
if (pos == 0) {
// this is just the table stats, not an index
continue;
} else if (pos == 1) {
logger.debug("Found index " + name);
idx = new SQLIndex(name, !nonUnique, qualifier, type, filter);
idx.setClustered(isClustered);
if (name.equals(pkName)) {
final SQLIndex pkIndex = idx;
Runnable pkIndexUpdate = new Runnable() {
public void run() {
try {
targetTable.getPrimaryKeyIndexWithoutPopulating().updateToMatch(pkIndex, false);
} catch (SQLObjectException e) {
throw new SQLObjectRuntimeException(e);
}
}
};
try {
targetTable.getRunnableDispatcher().runInForeground(pkIndexUpdate);
} catch (SessionNotFoundException e) {
pkIndexUpdate.run();
}
idx = targetTable.getPrimaryKeyIndexWithoutPopulating();
} else {
indexes.add(idx);
}
}
//Child columns of pk fixed before this loop
if (!idx.isPrimaryKeyIndex()) {
logger.debug("Adding column " + colName + " to index " + idx.getName());
final SQLIndex nonPKIndex = idx;
Runnable indexChildRunner = new Runnable() {
public void run() {
if (!targetTable.isColumnsPopulated()) {
throw new IllegalStateException("Table " + targetTable +
" is missing columns, cannot populate indices.");
}
SQLColumn tableCol;
try {
tableCol = targetTable.getColumnByName(colName, false, true);
Column indexCol;
if (tableCol != null) {
indexCol = new Column(tableCol, aOrD);
} else {
indexCol = new Column(colName, aOrD); // probably an expression like "col1+col2"
}
nonPKIndex.addChild(indexCol);
} catch (SQLObjectException e) {
throw new SQLObjectRuntimeException(e);
}
}
};
try {
targetTable.getRunnableDispatcher().runInForeground(indexChildRunner);
} catch (SessionNotFoundException e) {
indexChildRunner.run();
}
}
}
rs.close();
rs = null;
return indexes;
} finally {
try {
if (rs != null)
rs.close();
} catch (SQLException ex) {
logger.error("Couldn't close result set", ex);
}
}
}
@Transient @Accessor
public boolean isPrimaryKeyIndex() {
if (getParent() == null) return false;
return getParent().isPrimaryKey(this);
}
@Override
public String toString() {
return getName();
}
/**
* Adds a column to the index. If col1 is null a NPE will be thrown. The
* given column must also be a child of the parent table. If this index is
* the primary key of the table the column will be added to a position in
* the index to match its position in the table. Since the position will be
* matched by the index the column must be moved to the correct location in
* the index before calling this method if it is being done for the primary
* key.
* <p>
* The column in the index will be defined as unspecified for its ascending
* or descending term.
*/
public void addIndexColumn(SQLColumn col) {
addIndexColumn(col, AscendDescend.UNSPECIFIED);
}
/**
* Adds a column to the index. If col1 is null a NPE will be thrown. The
* given column must also be a child of the parent table. If this index is
* the primary key of the table the column will be added to a position in
* the index to match its position in the table. Since the position will be
* matched by the index the column must be moved to the correct location in
* the index before calling this method if it is being done for the primary
* key.
*/
public void addIndexColumn(SQLColumn col1, AscendDescend aOrD) {
if (getParent() != null && !getParent().getColumnsWithoutPopulating().contains(col1))
throw new IllegalArgumentException("Cannot add " + col1 + " to " + this +
" because the column is not part of the table " + getParent());
if (indexOf(col1) != -1)
throw new IllegalArgumentException("Column " + col1 + " already exists in this index.");
Column col = new Column(col1, aOrD);
addIndexColumn(col);
}
public void addIndexColumn(String colName, AscendDescend aOrD) throws SQLObjectException {
Column col = new Column(colName, aOrD);
addIndexColumn(col);
}
public void addIndexColumn(SQLIndex.Column col) {
addIndexColumn(col, columns.size());
}
public void addIndexColumn(SQLIndex.Column col, int index) {
if (col != null && indexOf(col.getColumn()) != -1)
throw new IllegalArgumentException("Column " + col + " already exists in this index.");
if (isPrimaryKeyIndex()) {
index = getParent().getColumnsWithoutPopulating().indexOf(col.getColumn());
col.getColumn().setNullable(DatabaseMetaData.columnNoNulls);
}
columns.add(index, col);
col.setParent(this);
fireChildAdded(SQLIndex.Column.class, col, index);
if (isPrimaryKeyIndex()) {
getParent().updateRelationshipsForNewIndexColumn(col.getColumn());
}
}
/**
* Returns a copy of a SQLIndex from a given SQLIndex in a parent SQLTable.
* This appears to be mainly used for creating a SQLIndex for a copied table
* in the playpen when importing tables from a source database as part of
* the reverse engineering feature.
*
* @param source The source SQLIndex to copy
* @param parentTable The parent SQLTable of the source SQLIndex
* @return A copy of the given source SQLIndex.
* @throws SQLObjectException
*/
public static SQLIndex getDerivedInstance(SQLIndex source, SQLTable parentTable) throws SQLObjectException {
SQLIndex index = new SQLIndex();
index.setName(source.getName());
index.setUnique(source.isUnique());
index.setPopulated(source.isPopulated());
index.setType(source.getType());
index.setFilterCondition(source.getFilterCondition());
index.setQualifier(source.getQualifier());
index.setPhysicalName(source.getPhysicalName());
index.setClustered(source.isClustered());
for (Map.Entry<Class<? extends SQLObject>, Throwable> inaccessibleReason : source.getChildrenInaccessibleReasons().entrySet()) {
index.setChildrenInaccessibleReason(inaccessibleReason.getValue(), inaccessibleReason.getKey(), false);
}
for (Column column : source.getChildren(Column.class)) {
Column newColumn;
if (column.getColumn() != null) {
SQLColumn sqlColumn = findEquivalentColumnNotIncluded(parentTable, index,
column.getColumn());
if (sqlColumn == null) {
throw new SQLObjectException("Can not derive instance, because coulmn " +
column.getColumn().getName() + "is not found in parent table [" + parentTable.getName() +
"]");
}
newColumn = new Column(sqlColumn, column.getAscendingOrDescending());
} else {
newColumn = new Column(column.getName(), column.getAscendingOrDescending());
}
index.addChild(newColumn);
}
return index;
}
/**
* Returns a column that is equivalent to the given column in the parent
* table that has not been added to the given index. This is a helper method
* for updating the given index to another index based on another index.
*
* @param parentTable
* The column returned will be a child of this table.
* @param index
* The index that the column cannot be a child of already.
* @param column
* The column we are trying to find an equivalent version of. If
* the column is in the parent table then the same column will be
* returned. If the column is not in the parent table a different
* column will be returned that closely matches this column.
*/
private static SQLColumn findEquivalentColumnNotIncluded(SQLTable parentTable,
SQLIndex index, SQLColumn column) {
if (column.getParent().equals(parentTable)) return column;
for (SQLColumn existingCol : parentTable.getColumnsWithoutPopulating()) {
if (existingCol.getName().equals(column.getName()) &&
index.indexOf(existingCol) == -1) {
return existingCol;
}
}
return null;
}
/**
* Make this index's columns look like the columns in index. If this is the
* primary key index columns may be moved in the parent table.
*
* @param index
* The index who's columns are what we want in this index
* @throws SQLObjectException
* @throws ObjectDependentException
* @throws IllegalArgumentException
*/
public void makeColumnsLike(SQLIndex index) throws SQLObjectException, IllegalArgumentException, ObjectDependentException {
makeColumnsLike(index.getChildrenWithoutPopulating(Column.class));
}
public void makeColumnsLike(List<Column> sourceCols) throws SQLObjectException {
List<Column> originalCols = new ArrayList<Column>(columns);
for (Column c : originalCols) {
boolean remove = true;
for (Column sourceCol : sourceCols) {
if ((sourceCol.getColumn() == null && sourceCol.getName().equals(c.getName()))
|| (sourceCol.getColumn() != null && sourceCol.getColumn().equals(c.getColumn()))) {
remove = false;
c.updateToMatch(sourceCol);
break;
}
}
if (remove) {
if (isPrimaryKeyIndex()) {
getParent().moveAfterPK(c.getColumn());
} else {
removeColumn(c);
}
}
}
int insertIndex = 0;
for (Column c : sourceCols) {
int currentIndex;
if (c.getColumn() == null) {
currentIndex = -1;
for (int i = 0; i < originalCols.size(); i++) {
Column t = originalCols.get(i);
if (t.getName().equals(c.getName())) {
currentIndex = i;
break;
}
}
} else {
currentIndex = indexOf(c.getColumn());
}
if (isPrimaryKeyIndex()) {
if (currentIndex != -1 && currentIndex != insertIndex) {
getParent().changeColumnIndex(currentIndex, insertIndex, true);
} else {
SQLColumn equivalentCol = findEquivalentColumnNotIncluded(getParent(), this, c.getColumn());
getParent().changeColumnIndex(
getParent().getColumnsWithoutPopulating().indexOf(equivalentCol),
insertIndex, true);
}
getChild(insertIndex).setAscendingOrDescending(c.getAscendingOrDescending());
} else {
if (currentIndex != -1 && currentIndex != insertIndex) {
Column child = getChild(currentIndex);
removeColumn(child);
child.setAscendingOrDescending(c.getAscendingOrDescending());
addIndexColumn(child, insertIndex);
} else if (currentIndex == -1) {
Column newCol = new Column(c.getName(), c.getAscendingOrDescending());
newCol.setColumn(c.getColumn());
addChild(newCol);
}
}
insertIndex++;
}
}
@Override
protected boolean removeChildImpl(SPObject child) {
if (child instanceof SQLIndex.Column) {
return removeColumn((SQLIndex.Column) child);
} else {
throw new IllegalArgumentException("Cannot remove children of type "
+ child.getClass() + " from " + getName());
}
}
public boolean removeColumn(SQLIndex.Column col) {
int index = columns.indexOf(col);
if (index != -1) {
columns.remove(index);
fireChildRemoved(SQLIndex.Column.class, col, index);
if (col.getColumn() != null) {
col.getColumn().removeSPListener(col.targetColumnListener);
}
if (isPrimaryKeyIndex()) {
getParent().updateRelationshipsForRemovedIndexColumns(col.getColumn());
}
col.setParent(null);
return true;
}
return false;
}
/**
* Removes the given column from this index. This does not change the ordering
* of the columns in the table.
*/
public boolean removeColumn(SQLColumn col) {
for (Column colWrapper : columns) {
if (colWrapper.getColumn().equals(col)) {
return removeColumn(colWrapper);
}
}
return false;
}
public int childPositionOffset(Class<? extends SPObject> childType) {
if (childType == SQLIndex.Column.class) return 0;
throw new IllegalArgumentException("The type " + childType +
" is not a valid child type of " + getName());
}
public List<? extends SPObject> getDependencies() {
return Collections.emptyList();
}
public void removeDependency(SPObject dependency) {
for (SQLObject child : getChildren()) {
child.removeDependency(dependency);
}
}
@NonProperty
public List<Class<? extends SPObject>> getAllowedChildTypes() {
return allowedChildTypes;
}
/**
* Returns true if there is a {@link Column} wrapper that points to the
* given column.
*/
public boolean containsColumn(SQLColumn column) {
for (Column colWrapper : columns) {
if (colWrapper.getColumn().equals(column)) {
return true;
}
}
return false;
}
/**
* Helper method for finding the index of a {@link SQLColumn} inside this
* index. Returns the index in this column or -1 if the column does not
* exist as a wrapped child of this index.
*/
public int indexOf(SQLColumn col) {
for (int i = 0; i < columns.size(); i++) {
if (columns.get(i).getColumn() != null &&
columns.get(i).getColumn().equals(col)) {
return i;
}
}
return -1;
}
/**
* Returns true if there are columns in this index.
* @return
*/
@Transient @Accessor
public boolean isEmpty() {
return columns.isEmpty();
}
}