/* Copyright (c) 1995-2000, The Hypersonic SQL Group. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of the Hypersonic SQL Group nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE HYPERSONIC SQL GROUP, * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * This software consists of voluntary contributions made by many individuals * on behalf of the Hypersonic SQL Group. * * * For work added by the HSQL Development Group: * * Copyright (c) 2001-2009, The HSQL Development Group * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of the HSQL Development Group nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG, * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.hsqldb; import org.hsqldb.HSQLInterface.HSQLParseException; import org.hsqldb.HsqlNameManager.HsqlName; import org.hsqldb.RangeVariable.RangeIteratorBase; import org.hsqldb.index.Index; import org.hsqldb.lib.ArrayUtil; import org.hsqldb.lib.OrderedHashSet; import org.hsqldb.navigator.RowIterator; import org.hsqldb.persist.PersistentStore; import org.hsqldb.result.Result; import org.hsqldb.rights.Grantee; // fredt@users 20020225 - patch 1.7.0 by boucherb@users - named constraints // fredt@users 20020320 - doc 1.7.0 - update // tony_lai@users 20020820 - patch 595156 - violation of Integrity constraint name /** * Implementation of a table constraint with references to the indexes used * by the constraint.<p> * * Partly based on Hypersonic code. * * @author Thomas Mueller (Hypersonic SQL Group) * @author Fred Toussi (fredt@users dot sourceforge.net) * @version 1.9.0 * @since Hypersonic SQL */ public final class Constraint implements SchemaObject { /* SQL CLI codes Referential Constraint 0 CASCADE Referential Constraint 1 RESTRICT Referential Constraint 2 SET NULL Referential Constraint 3 NO ACTION Referential Constraint 4 SET DEFAULT */ public static final int CASCADE = 0, RESTRICT = 1, SET_NULL = 2, NO_ACTION = 3, SET_DEFAULT = 4, INIT_DEFERRED = 5, INIT_IMMEDIATE = 6, NOT_DEFERRABLE = 7; public static final int FOREIGN_KEY = 0, MAIN = 1, UNIQUE = 2, CHECK = 3, PRIMARY_KEY = 4, TEMP = 5; ConstraintCore core; private HsqlName name; int constType; boolean isForward; // Expression check; String checkStatement; private boolean isNotNull; int notNullColumnIndex; RangeVariable rangeVariable; OrderedHashSet schemaObjectNames; // for temp constraints only OrderedHashSet mainColSet; OrderedHashSet refColSet; // final public static Constraint[] emptyArray = new Constraint[]{}; /** * Constructor declaration for PK and UNIQUE */ public Constraint(HsqlName name, Table t, Index index, int type) { core = new ConstraintCore(); this.name = name; constType = type; core.mainTable = t; core.mainIndex = index; core.mainCols = index.getColumns(); } /** * Constructor for main constraints (foreign key references in PK table) */ public Constraint(HsqlName name, Constraint fkconstraint) { this.name = name; constType = MAIN; core = fkconstraint.core; } Constraint duplicate() { Constraint copy = new Constraint(); copy.core = core.duplicate(); copy.name = name; copy.constType = constType; copy.isForward = isForward; // copy.check = check; copy.isNotNull = isNotNull; copy.notNullColumnIndex = notNullColumnIndex; copy.rangeVariable = rangeVariable; copy.schemaObjectNames = schemaObjectNames; return copy; } /** * General constructor for foreign key constraints. * * @param name name of constraint * @param refCols list of referencing columns * @param mainTableName referenced table * @param mainCols list of referenced columns * @param type constraint type * @param deleteAction triggered action on delete * @param updateAction triggered action on update * */ public Constraint(HsqlName name, HsqlName refTableName, OrderedHashSet refCols, HsqlName mainTableName, OrderedHashSet mainCols, int type, int deleteAction, int updateAction, int matchType) { core = new ConstraintCore(); this.name = name; constType = type; mainColSet = mainCols; core.refTableName = refTableName; core.mainTableName = mainTableName; refColSet = refCols; core.deleteAction = deleteAction; core.updateAction = updateAction; core.matchType = matchType; } public Constraint(HsqlName name, OrderedHashSet mainCols, int type) { core = new ConstraintCore(); this.name = name; constType = type; mainColSet = mainCols; } void setColumnsIndexes(Table table) { if (constType == Constraint.FOREIGN_KEY) { if (mainColSet == null) { core.mainCols = core.mainTable.getPrimaryKey(); if (core.mainCols == null) { throw Error.error(ErrorCode.X_42581); } } else if (core.mainCols == null) { core.mainCols = core.mainTable.getColumnIndexes(mainColSet); } if (core.refCols == null) { core.refCols = table.getColumnIndexes(refColSet); } } else if (mainColSet != null) { core.mainCols = table.getColumnIndexes(mainColSet); } } private Constraint() {} public int getType() { return SchemaObject.CONSTRAINT; } /** * Returns the HsqlName. */ public HsqlName getName() { return name; } public HsqlName getCatalogName() { return name.schema.schema; } public HsqlName getSchemaName() { return name.schema; } public Grantee getOwner() { return name.schema.owner; } public OrderedHashSet getReferences() { switch (constType) { case Constraint.CHECK : return schemaObjectNames; case Constraint.FOREIGN_KEY : OrderedHashSet set = new OrderedHashSet(); set.add(core.uniqueName); return set; } return null; } public OrderedHashSet getComponents() { return null; } public void compile(Session session) {} public String getSQL() { StringBuffer sb = new StringBuffer(); switch (getConstraintType()) { case Constraint.PRIMARY_KEY : if (getMainColumns().length > 1 || (getMainColumns().length == 1 && !getName().isReservedName())) { if (!getName().isReservedName()) { sb.append(Tokens.T_CONSTRAINT).append(' '); sb.append(getName().statementName).append(' '); } sb.append(Tokens.T_PRIMARY).append(' ').append( Tokens.T_KEY); getColumnList(getMain(), getMainColumns(), getMainColumns().length, sb); } break; case Constraint.UNIQUE : if (!getName().isReservedName()) { sb.append(Tokens.T_CONSTRAINT).append(' '); sb.append(getName().statementName); sb.append(' '); } sb.append(Tokens.T_UNIQUE); int[] col = getMainColumns(); getColumnList(getMain(), col, col.length, sb); break; case Constraint.FOREIGN_KEY : if (isForward) { sb.append(Tokens.T_ALTER).append(' ').append( Tokens.T_TABLE).append(' '); sb.append( getRef().getName().getSchemaQualifiedStatementName()); sb.append(' ').append(Tokens.T_ADD).append(' '); getFKStatement(sb); } else { getFKStatement(sb); } break; case Constraint.CHECK : if (isNotNull()) { break; } if (!getName().isReservedName()) { sb.append(Tokens.T_CONSTRAINT).append(' '); sb.append(getName().statementName).append(' '); } sb.append(Tokens.T_CHECK).append('('); sb.append(check.getSQL()); sb.append(')'); // should not throw as it is already tested OK break; } return sb.toString(); } /** * Generates the foreign key declaration for a given Constraint object. */ private void getFKStatement(StringBuffer a) { if (!getName().isReservedName()) { a.append(Tokens.T_CONSTRAINT).append(' '); a.append(getName().statementName); a.append(' '); } a.append(Tokens.T_FOREIGN).append(' ').append(Tokens.T_KEY); int[] col = getRefColumns(); getColumnList(getRef(), col, col.length, a); a.append(' ').append(Tokens.T_REFERENCES).append(' '); a.append(getMain().getName().getSchemaQualifiedStatementName()); col = getMainColumns(); getColumnList(getMain(), col, col.length, a); if (getDeleteAction() != Constraint.NO_ACTION) { a.append(' ').append(Tokens.T_ON).append(' ').append( Tokens.T_DELETE).append(' '); a.append(getDeleteActionString()); } if (getUpdateAction() != Constraint.NO_ACTION) { a.append(' ').append(Tokens.T_ON).append(' ').append( Tokens.T_UPDATE).append(' '); a.append(getUpdateActionString()); } } /** * Generates the column definitions for a table. */ private static void getColumnList(Table t, int[] col, int len, StringBuffer a) { a.append('('); for (int i = 0; i < len; i++) { a.append(t.getColumn(col[i]).getName().statementName); if (i < len - 1) { a.append(','); } } a.append(')'); } public HsqlName getMainTableName() { return core.mainTableName; } public HsqlName getMainName() { return core.mainName; } public HsqlName getRefName() { return core.refName; } public HsqlName getUniqueName() { return core.uniqueName; } /** * Returns the type of constraint */ public int getConstraintType() { return constType; } /** * Returns the main table */ public Table getMain() { return core.mainTable; } /** * Returns the main index */ Index getMainIndex() { return core.mainIndex; } /** * Returns the reference table */ public Table getRef() { return core.refTable; } /** * Returns the reference index */ Index getRefIndex() { return core.refIndex; } /** * Returns the foreign key action rule. */ private static String getActionString(int action) { switch (action) { case Constraint.RESTRICT : return Tokens.T_RESTRICT; case Constraint.CASCADE : return Tokens.T_CASCADE; case Constraint.SET_DEFAULT : return Tokens.T_SET + ' ' + Tokens.T_DEFAULT; case Constraint.SET_NULL : return Tokens.T_SET + ' ' + Tokens.T_NULL; default : return Tokens.T_NO + ' ' + Tokens.T_ACTION; } } /** * The ON DELETE triggered action of (foreign key) constraint */ public int getDeleteAction() { return core.deleteAction; } public String getDeleteActionString() { return getActionString(core.deleteAction); } /** * The ON UPDATE triggered action of (foreign key) constraint */ public int getUpdateAction() { return core.updateAction; } public String getUpdateActionString() { return getActionString(core.updateAction); } public boolean hasTriggeredAction() { if (constType == Constraint.FOREIGN_KEY) { switch (core.deleteAction) { case Constraint.CASCADE : case Constraint.SET_DEFAULT : case Constraint.SET_NULL : return true; } switch (core.updateAction) { case Constraint.CASCADE : case Constraint.SET_DEFAULT : case Constraint.SET_NULL : return true; } } return false; } public int getDeferability() { return NOT_DEFERRABLE; } /** * Returns the main table column index array */ public int[] getMainColumns() { return core.mainCols; } /** * Returns the reference table column index array */ public int[] getRefColumns() { return core.refCols; } /** * Returns the SQL for the expression in CHECK clause */ public String getCheckSQL() { return check.getSQL(); } /** * Returns true if the expression in CHECK is a simple IS NOT NULL */ public boolean isNotNull() { return isNotNull; } boolean hasColumnOnly(int colIndex) { switch (constType) { case CHECK : return rangeVariable.usedColumns[colIndex] && ArrayUtil .countTrueElements(rangeVariable.usedColumns) == 1; case PRIMARY_KEY : case UNIQUE : return core.mainCols.length == 1 && core.mainCols[0] == colIndex; case MAIN : return core.mainCols.length == 1 && core.mainCols[0] == colIndex && core.mainTable == core.refTable; case FOREIGN_KEY : return core.refCols.length == 1 && core.refCols[0] == colIndex && core.mainTable == core.refTable; default : throw Error.runtimeError(ErrorCode.U_S0500, "Constraint"); } } boolean hasColumnPlus(int colIndex) { switch (constType) { case CHECK : return rangeVariable.usedColumns[colIndex] && ArrayUtil .countTrueElements(rangeVariable.usedColumns) > 1; case PRIMARY_KEY : case UNIQUE : return core.mainCols.length != 1 && ArrayUtil.find(core.mainCols, colIndex) != -1; case MAIN : return ArrayUtil.find(core.mainCols, colIndex) != -1 && (core.mainCols.length != 1 || core.mainTable != core.refTable); case FOREIGN_KEY : return ArrayUtil.find(core.refCols, colIndex) != -1 && (core.mainCols.length != 1 || core.mainTable == core.refTable); default : throw Error.runtimeError(ErrorCode.U_S0500, "Constraint"); } } boolean hasColumn(int colIndex) { switch (constType) { case CHECK : return rangeVariable.usedColumns[colIndex]; case PRIMARY_KEY : case UNIQUE : case MAIN : return ArrayUtil.find(core.mainCols, colIndex) != -1; case FOREIGN_KEY : return ArrayUtil.find(core.refCols, colIndex) != -1; default : throw Error.runtimeError(ErrorCode.U_S0500, "Constraint"); } } // fredt@users 20020225 - patch 1.7.0 by fredt - duplicate constraints /** * Compares this with another constraint column set. This is used only for * UNIQUE constraints. */ boolean isUniqueWithColumns(int[] cols) { if (constType != UNIQUE || core.mainCols.length != cols.length) { return false; } return ArrayUtil.haveEqualSets(core.mainCols, cols, cols.length); } /** * Compares this with another constraint column set. This implementation * only checks FOREIGN KEY constraints. */ boolean isEquivalent(Table mainTable, int[] mainCols, Table refTable, int[] refCols) { if (constType != Constraint.MAIN && constType != Constraint.FOREIGN_KEY) { return false; } if (mainTable != core.mainTable || refTable != core.refTable) { return false; } return ArrayUtil.areEqualSets(core.mainCols, mainCols) && ArrayUtil.areEqualSets(core.refCols, refCols); } /** * Used to update constrains to reflect structural changes in a table. Prior * checks must ensure that this method does not throw. * * @param session Session * @param oldTable reference to the old version of the table * @param newTable referenct to the new version of the table * @param colIndex index at which table column is added or removed * @param adjust -1, 0, +1 to indicate if column is added or removed * @ */ void updateTable(Session session, Table oldTable, Table newTable, int colIndex, int adjust) { if (oldTable == core.mainTable) { core.mainTable = newTable; if (core.mainIndex != null) { core.mainIndex = core.mainTable.getIndex(core.mainIndex.getName().name); core.mainCols = ArrayUtil.toAdjustedColumnArray(core.mainCols, colIndex, adjust); } } if (oldTable == core.refTable) { core.refTable = newTable; if (core.refIndex != null) { core.refIndex = core.refTable.getIndex(core.refIndex.getName().name); core.refCols = ArrayUtil.toAdjustedColumnArray(core.refCols, colIndex, adjust); } } // CHECK if (constType == CHECK) { recompile(session, newTable); } } /** * Checks for foreign key or check constraint violation when * inserting a row into the child table. */ void checkInsert(Session session, Table table, Object[] row) { switch (constType) { case CHECK : if (!isNotNull) { checkCheckConstraint(session, table, row); } return; case FOREIGN_KEY : PersistentStore store = session.sessionData.getRowStore(core.mainTable); if (ArrayUtil.hasNull(row, core.refCols)) { if (core.matchType == OpTypes.MATCH_SIMPLE) { return; } if (core.refCols.length == 1) { return; } if (ArrayUtil.hasAllNull(row, core.refCols)) { return; } // core.matchType == OpTypes.MATCH_FULL } else if (core.mainIndex.exists(session, store, row, core.refCols)) { return; } else if (core.mainTable == core.refTable) { // special case: self referencing table and self referencing row int compare = core.mainIndex.compareRowNonUnique(row, core.refCols, row); if (compare == 0) { return; } } String[] info = new String[] { core.refName.name, core.mainTable.getName().name }; throw Error.error(ErrorCode.X_23502, ErrorCode.CONSTRAINT, info); } } /* * Tests a row against this CHECK constraint. */ void checkCheckConstraint(Session session, Table table, Object[] data) { /* if (session.compiledStatementExecutor.rangeIterators[1] == null) { session.compiledStatementExecutor.rangeIterators[1] = rangeVariable.getIterator(session); } */ RangeIteratorBase it = (RangeIteratorBase) session.sessionContext.getCheckIterator(); if (it == null) { it = rangeVariable.getIterator(session); session.sessionContext.setCheckIterator(it); } it.currentData = data; boolean nomatch = Boolean.FALSE.equals(check.getValue(session)); it.currentData = null; if (nomatch) { String[] info = new String[] { name.name, table.tableName.name }; throw Error.error(ErrorCode.X_23504, ErrorCode.CONSTRAINT, info); } } void checkCheckConstraint(Session session, Table table, Object data) { session.sessionData.currentValue = data; boolean nomatch = Boolean.FALSE.equals(check.getValue(session)); session.sessionData.currentValue = null; if (nomatch) { if (table == null) { throw Error.error(ErrorCode.X_23504, name.name); } else { String[] info = new String[] { name.name, table.tableName.name }; throw Error.error(ErrorCode.X_23504, ErrorCode.CONSTRAINT, info); } } } // fredt@users 20020225 - patch 1.7.0 - cascading deletes /** * New method to find any referencing row for a foreign key (finds row in * child table). If ON DELETE CASCADE is specified for this constraint, then * the method finds the first row among the rows of the table ordered by the * index and doesn't throw. Without ON DELETE CASCADE, the method attempts * to finds any row that exists. If no * row is found, null is returned. (fredt@users) * * @param session Session * @param row array of objects for a database row * @param delete should we allow 'ON DELETE CASCADE' or 'ON UPDATE CASCADE' * @return iterator * @ */ RowIterator findFkRef(Session session, Object[] row, boolean delete) { if (row == null || ArrayUtil.hasNull(row, core.mainCols)) { return core.refIndex.emptyIterator(); } PersistentStore store = session.sessionData.getRowStore(core.refTable); return core.refIndex.findFirstRow(session, store, row, core.mainCols); } /** * For the candidate table row, finds any referring node in the main table. * This is used to check referential integrity when updating a node. We * have to make sure that the main table still holds a valid main record. * returns true If a valid row is found, false if there are null in the data * Otherwise a 'INTEGRITY VIOLATION' Exception gets thrown. */ boolean checkHasMainRef(Session session, Object[] row) { if (ArrayUtil.hasNull(row, core.refCols)) { return false; } PersistentStore store = session.sessionData.getRowStore(core.mainTable); boolean exists = core.mainIndex.exists(session, store, row, core.refCols); if (!exists) { String[] info = new String[] { core.refName.name, core.mainTable.getName().name }; throw Error.error(ErrorCode.X_23502, ErrorCode.CONSTRAINT, info); } return exists; } /** * Check used before creating a new foreign key cosntraint, this method * checks all rows of a table to ensure they all have a corresponding * row in the main table. */ void checkReferencedRows(Session session, Table table, int[] rowColArray) { Index mainIndex = getMainIndex(); PersistentStore store = session.sessionData.getRowStore(table); RowIterator it = table.rowIterator(session); while (true) { Row row = it.getNextRow(); if (row == null) { break; } Object[] rowData = row.getData(); if (ArrayUtil.hasNull(rowData, rowColArray)) { if (core.matchType == OpTypes.MATCH_SIMPLE) { continue; } } else if (mainIndex.exists(session, store, rowData, rowColArray)) { continue; } if (ArrayUtil.hasAllNull(rowData, rowColArray)) { continue; } String colValues = ""; for (int i = 0; i < rowColArray.length; i++) { Object o = rowData[rowColArray[i]]; colValues += table.getColumnTypes()[i].convertToString(o); colValues += ","; } String[] info = new String[] { getName().name, getMain().getName().name }; throw Error.error(ErrorCode.X_23502, ErrorCode.CONSTRAINT, info); } } public Expression getCheckExpression() { return check; } public OrderedHashSet getCheckColumnExpressions() { OrderedHashSet set = new OrderedHashSet(); Expression.collectAllExpressions(set, check, Expression.columnExpressionSet, Expression.emptyExpressionSet); return set; } void recompile(Session session, Table newTable) { String ddl = check.getSQL(); Scanner scanner = new Scanner(ddl); ParserDQL parser = new ParserDQL(session, scanner); parser.read(); parser.isCheckOrTriggerCondition = true; Expression condition = parser.XreadBooleanValueExpression(); check = condition; schemaObjectNames = parser.compileContext.getSchemaObjectNames(); // this workaround is here to stop LIKE optimisation (for proper scripting) QuerySpecification s = Expression.getCheckSelect(session, newTable, check); rangeVariable = s.rangeVariables[0]; rangeVariable.setForCheckConstraint(); } void prepareCheckConstraint(Session session, Table table, boolean checkValues) { // to ensure no subselects etc. are in condition check.checkValidCheckConstraint(); if (table == null) { check.resolveTypes(session, null); } else { QuerySpecification s = Expression.getCheckSelect(session, table, check); Result r = s.getResult(session, 1); if (r.getNavigator().getSize() != 0) { String[] info = new String[] { table.getName().name, "" }; throw Error.error(ErrorCode.X_23504, ErrorCode.CONSTRAINT, info); } rangeVariable = s.rangeVariables[0]; // removes reference to the Index object in range variable rangeVariable.setForCheckConstraint(); } if (check.getType() == OpTypes.NOT && check.getLeftNode().getType() == OpTypes.IS_NULL && check.getLeftNode().getLeftNode().getType() == OpTypes.COLUMN) { notNullColumnIndex = check.getLeftNode().getLeftNode().getColumnIndex(); isNotNull = true; } } /*************** VOLTDB *********************/ /** * @return The name of this constraint instance's type. */ String getTypeName() { switch (constType) { case FOREIGN_KEY: return "FOREIGN_KEY"; case MAIN: return "MAIN"; case UNIQUE: return "UNIQUE"; case CHECK: return "CHECK"; case PRIMARY_KEY: return "PRIMARY_KEY"; } return "UNKNOWN"; } /** * VoltDB added method to get a non-catalog-dependent * representation of this HSQLDB object. * @param session The current Session object may be needed to resolve * some names. * @param indent A string of whitespace to be prepended to every line * in the resulting XML. * @return XML, correctly indented, representing this object. */ String voltGetXML(Session session, String indent) throws HSQLParseException { StringBuilder sb = new StringBuilder(); // Skip "MAIN" constraints, as they are the parent of foreign key references if (this.constType != MAIN) { sb.append(indent).append("<constraint"); sb.append(" name='").append(getName().name).append("'"); sb.append(" type='").append(getTypeName()).append("'"); // Foreign Keys if (this.constType == FOREIGN_KEY) { Table our_table = this.getRef(); int our_cols[] = this.getRefColumns(); Table fkey_table = this.getMain(); int fkey_cols[] = this.getMainColumns(); if (core.refIndex != null) sb.append(" index='").append(core.refIndex.getName().name).append("'"); else sb.append(" index=''"); sb.append(" foreignkeytable='").append(fkey_table.getName().statementName).append("'"); sb.append(" >\n"); // XXX Can bad SQL get us here or does HSQL barf before that? assert(our_cols.length == fkey_cols.length); for (int i = 0; i < our_cols.length; i++) { String our_colname = our_table.getColumn(our_cols[i]).getName().statementName; String fkey_colname = fkey_table.getColumn(fkey_cols[i]).getName().statementName; sb.append(indent).append(indent).append("<reference"); sb.append(" from='").append(our_colname).append("'"); sb.append(" to='").append(fkey_colname).append("'"); sb.append(" />\n"); } sb.append(indent).append("</constraint>\n"); // All other constraints... } else { if (core.mainIndex != null) sb.append(" index='").append(core.mainIndex.getName().name).append("'"); else sb.append(" index=''"); sb.append(" />\n"); } } return sb.toString(); } }