You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openjpa.apache.org by de...@apache.org on 2009/12/11 21:06:19 UTC

svn commit: r889793 - in /openjpa/branches/1.1.x: openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/ openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/ openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/ openjpa-persiste...

Author: dezzio
Date: Fri Dec 11 20:06:18 2009
New Revision: 889793

URL: http://svn.apache.org/viewvc?rev=889793&view=rev
Log:
OpenJPA-817: Applied Ravi Palacherla's patch to 1.1.x branch.  Also made ConstraintUpdateManager's EOL native.

Added:
    openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Employee.java   (with props)
    openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Story.java   (with props)
    openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Task.java   (with props)
    openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/TestInsertOrder.java   (with props)
Modified:
    openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/ConstraintUpdateManager.java   (contents, props changed)
    openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/RowManagerImpl.java
    openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/test/SQLListenerTestCase.java

Modified: openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/ConstraintUpdateManager.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/ConstraintUpdateManager.java?rev=889793&r1=889792&r2=889793&view=diff
==============================================================================
--- openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/ConstraintUpdateManager.java (original)
+++ openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/ConstraintUpdateManager.java Fri Dec 11 20:06:18 2009
@@ -1,546 +1,546 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.openjpa.jdbc.kernel;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.openjpa.jdbc.meta.ClassMapping;
-import org.apache.openjpa.jdbc.schema.Column;
-import org.apache.openjpa.jdbc.schema.ForeignKey;
-import org.apache.openjpa.jdbc.schema.Table;
-import org.apache.openjpa.jdbc.sql.PrimaryRow;
-import org.apache.openjpa.jdbc.sql.Row;
-import org.apache.openjpa.jdbc.sql.RowImpl;
-import org.apache.openjpa.jdbc.sql.RowManager;
-import org.apache.openjpa.jdbc.sql.RowManagerImpl;
-import org.apache.openjpa.jdbc.sql.SQLExceptions;
-import org.apache.openjpa.kernel.OpenJPAStateManager;
-import org.apache.openjpa.lib.graph.DepthFirstAnalysis;
-import org.apache.openjpa.lib.graph.Edge;
-import org.apache.openjpa.lib.graph.Graph;
-import org.apache.openjpa.lib.util.Localizer;
-import org.apache.openjpa.util.InternalException;
-import org.apache.openjpa.util.OpenJPAException;
-import org.apache.openjpa.util.UserException;
-
-/**
- * <p>Standard update manager, capable of foreign key constraint evaluation.</p>
- *
- * @since 1.0.0
- */
-public class ConstraintUpdateManager
-    extends AbstractUpdateManager {
-
-    private static final Localizer _loc = Localizer.forPackage
-        (ConstraintUpdateManager.class);
-
-    public boolean orderDirty() {
-        return false;
-    }
-
-    protected PreparedStatementManager newPreparedStatementManager
-        (JDBCStore store, Connection conn) {
-        return new PreparedStatementManagerImpl(store, conn);
-    }
-
-    protected RowManager newRowManager() {
-        return new RowManagerImpl(false);
-    }
-
-    protected Collection flush(RowManager rowMgr,
-        PreparedStatementManager psMgr, Collection exceps) {
-        RowManagerImpl rmimpl = (RowManagerImpl) rowMgr;
-
-        // first take care of all secondary table deletes and 'all row' deletes
-        // (which are probably secondary table deletes), since no foreign
-        // keys ever rely on secondary table pks
-        flush(rmimpl.getAllRowDeletes(), psMgr);
-        flush(rmimpl.getSecondaryDeletes(), psMgr);
-
-        // now do any 'all row' updates
-        flush(rmimpl.getAllRowUpdates(), psMgr);
-
-        // analyze foreign keys
-        Collection inserts = rmimpl.getInserts();
-        Collection updates = rmimpl.getUpdates();
-        Collection deletes = rmimpl.getDeletes();
-        Graph[] graphs = new Graph[2];    // insert graph, delete graph
-        analyzeForeignKeys(inserts, updates, deletes, rmimpl, graphs);
-
-        // flush insert graph, if any
-        boolean autoAssign = rmimpl.hasAutoAssignConstraints();
-        try {
-            flushGraph(graphs[0], psMgr, autoAssign);
-        } catch (SQLException se) {
-            exceps = addException(exceps, SQLExceptions.getStore(se, dict));
-        } catch (OpenJPAException ke) {
-            exceps = addException(exceps, ke);
-        }
-
-        // flush the rest of the inserts and updates; inserts before updates
-        // because some update fks might reference pks that have to be inserted
-        flush(inserts, psMgr);
-        flush(updates, psMgr);
-
-        // flush the delete graph, if any
-        try {
-            flushGraph(graphs[1], psMgr, autoAssign);
-        } catch (SQLException se) {
-            exceps = addException(exceps, SQLExceptions.getStore(se, dict));
-        } catch (OpenJPAException ke) {
-            exceps = addException(exceps, ke);
-        }
-
-        // put the remainder of the deletes after updates because some updates
-        // may be nulling fks to rows that are going to be deleted
-        flush(deletes, psMgr);
-
-        // take care of all secondary table inserts and updates last, since
-        // they may rely on previous inserts or updates, but nothing relies
-        // on them
-        flush(rmimpl.getSecondaryUpdates(), psMgr);
-
-        // flush any left over prepared statements
-        psMgr.flush();
-        return exceps;
-    }
-
-    /**
-     * Analyze foreign key dependencies on the given rows
-     * and create an insert and a delete graph to execute.  The insert
-     * graph will be flushed before all other rows, and the delete graph will
-     * be flushed after them.
-     */
-    private void analyzeForeignKeys(Collection inserts, Collection updates,
-        Collection deletes, RowManagerImpl rowMgr, Graph[] graphs) {
-        // if there are any deletes, we have to map the insert objects on their
-        // oids so we'll be able to detect delete-then-insert-same-pk cases
-        Map insertMap = null;
-        OpenJPAStateManager sm;
-        if (!deletes.isEmpty() && !inserts.isEmpty()) {
-            insertMap = new HashMap((int) (inserts.size() * 1.33 + 1));
-            for (Iterator itr = inserts.iterator(); itr.hasNext();) {
-                sm = ((Row) itr.next()).getPrimaryKey();
-                if (sm != null && sm.getObjectId() != null)
-                    insertMap.put(sm.getObjectId(), sm);
-            }
-        }
-
-        // first construct the graph for deletes; this may expand to include
-        // inserts and updates as well if there are any inserts that rely on
-        // deletes (delete-then-insert-same-pk cases)
-        PrimaryRow row;
-        Row row2;
-        ForeignKey[] fks;
-        OpenJPAStateManager fkVal;
-        boolean ignoreUpdates = true;
-        for (Iterator itr = deletes.iterator(); itr.hasNext();) {
-            row = (PrimaryRow) itr.next();
-            if (!row.isValid())
-                continue;
-
-            row2 = getInsertRow(insertMap, rowMgr, row);
-            if (row2 != null) {
-                ignoreUpdates = false;
-                graphs[1] = addEdge(graphs[1], (PrimaryRow) row2, row, null);
-            }
-
-            // now check this row's fks against other deletes
-            fks = row.getTable().getForeignKeys();
-            for (int j = 0; j < fks.length; j++) {
-                // when deleting ref fks they'll just set a where value, so
-                // check both for fk updates (relation fks) and wheres (ref fks)
-                fkVal = row.getForeignKeySet(fks[j]);
-                if (fkVal == null)
-                    fkVal = row.getForeignKeyWhere(fks[j]);
-                if (fkVal == null)
-                    continue;
-
-                row2 = rowMgr.getRow(fks[j].getPrimaryKeyTable(),
-                    Row.ACTION_DELETE, fkVal, false);
-                if (row2 != null && row2.isValid() && row2 != row)
-                    graphs[1] = addEdge(graphs[1], (PrimaryRow) row2, row,
-                        fks[j]);
-            }
-        }
-
-        if (ignoreUpdates)
-            graphs[0] = analyzeAgainstInserts(inserts, rowMgr, graphs[0]);
-        else {
-            // put inserts *and updates* in the delete graph; they all rely
-            // on each other
-            graphs[1] = analyzeAgainstInserts(updates, rowMgr, graphs[1]);
-            graphs[1] = analyzeAgainstInserts(inserts, rowMgr, graphs[1]);
-        }
-    }
-
-    /**
-     * Check to see if there is an insert for for the same table and primary
-     * key values as the given delete row.
-     */
-    private Row getInsertRow(Map insertMap, RowManagerImpl rowMgr, Row row) {
-        if (insertMap == null)
-            return null;
-
-        OpenJPAStateManager sm = row.getPrimaryKey();
-        if (sm == null)
-            return null;
-
-        // look for a new object whose insert id is the same as this delete one
-        Object oid = sm.getObjectId();
-        OpenJPAStateManager nsm = (OpenJPAStateManager) insertMap.get(oid);
-        if (nsm == null)
-            return null;
-
-        // found new object; get its row
-        row = rowMgr.getRow(row.getTable(), Row.ACTION_INSERT, nsm, false);
-        return (row == null || row.isValid()) ? row : null;
-    }
-
-    /**
-     * Analyze the given rows against the inserts, placing dependencies
-     * in the given graph.
-     */
-    private Graph analyzeAgainstInserts(Collection rows, RowManagerImpl rowMgr,
-        Graph graph) {
-        PrimaryRow row;
-        Row row2;
-        ForeignKey[] fks;
-        Column[] cols;
-        for (Iterator itr = rows.iterator(); itr.hasNext();) {
-            row = (PrimaryRow) itr.next();
-            if (!row.isValid())
-                continue;
-
-            // check this row's fks against inserts; a logical fk to an auto-inc
-            // column is treated just as actual database fk because the result
-            // is the same: the pk row has to be inserted before the fk row
-            fks = row.getTable().getForeignKeys();
-            for (int j = 0; j < fks.length; j++) {
-                if (row.getForeignKeySet(fks[j]) == null)
-                    continue;
-
-                // see if this row is dependent on another.  if it's only
-                // depenent on itself, see if the fk is logical or deferred, in
-                // which case it must be an auto-inc because otherwise we
-                // wouldn't have recorded it
-                row2 = rowMgr.getRow(fks[j].getPrimaryKeyTable(),
-                    Row.ACTION_INSERT, row.getForeignKeySet(fks[j]), false);
-                if (row2 != null && row2.isValid() && (row2 != row
-                    || fks[j].isDeferred() || fks[j].isLogical()))
-                    graph = addEdge(graph, row, (PrimaryRow) row2, fks[j]);
-            }
-
-            // see if there are any relation id columns dependent on
-            // auto-inc objects
-            cols = row.getTable().getRelationIdColumns();
-            for (int j = 0; j < cols.length; j++) {
-                OpenJPAStateManager sm = row.getRelationIdSet(cols[j]);
-                if (sm == null)
-                    continue;
-
-                row2 = rowMgr.getRow(getBaseTable(sm), Row.ACTION_INSERT,
-                    sm, false);
-                if (row2 != null && row2.isValid())
-                    graph = addEdge(graph, row, (PrimaryRow) row2, cols[j]);
-            }
-        }
-        return graph;
-    }
-
-    /**
-     * Return the base table for the given instance.
-     */
-    private static Table getBaseTable(OpenJPAStateManager sm) {
-        ClassMapping cls = (ClassMapping) sm.getMetaData();
-        while (cls.getJoinablePCSuperclassMapping() != null)
-            cls = cls.getJoinablePCSuperclassMapping();
-        return cls.getTable();
-    }
-
-    /**
-     * Add an edge between the given rows in the given foreign key graph.
-     */
-    private Graph addEdge(Graph graph, PrimaryRow row1, PrimaryRow row2,
-        Object fk) {
-        // delay creation of the graph
-        if (graph == null)
-            graph = new Graph();
-
-        row1.setDependent(true);
-        row2.setDependent(true);
-        graph.addNode(row1);
-        graph.addNode(row2);
-
-        // add an edge from row1 to row2, and set the fk causing the
-        // dependency as the user object so we can retrieve it when resolving
-        // circular constraints
-        Edge edge = new Edge(row1, row2, true);
-        edge.setUserObject(fk);
-        graph.addEdge(edge);
-
-        return graph;
-    }
-
-    /**
-     * Flush the given graph of rows in the proper order.
-     * @param graph The graph of statements to be walked
-     * @param psMgr The prepared statement manager to use to issue the
-     * statements
-     * @param autoAssign Whether any of the rows in the graph have any
-     * auto-assign constraints
-     */
-    protected void flushGraph(Graph graph, PreparedStatementManager psMgr,
-        boolean autoAssign)
-        throws SQLException {
-        if (graph == null)
-            return;
-
-        DepthFirstAnalysis dfa = newDepthFirstAnalysis(graph, autoAssign);
-        Collection insertUpdates = new LinkedList();
-        Collection deleteUpdates = new LinkedList();
-        boolean recalculate;
-
-        // Handle circular constraints:
-        // - if deleted row A has a ciricular fk to deleted row B, 
-        //   then use an update statement to null A's fk to B before flushing, 
-        //   and then flush
-        // - if inserted row A has a circular fk to updated/inserted row B,
-        //   then null the fk in the B row object, then flush,
-        //   and after flushing, use an update to set the fk back to A
-        // Depending on where circular dependencies are broken, the  
-        // topological order of the graph nodes has to be re-calculated.
-        recalculate = resolveCycles(graph, dfa.getEdges(Edge.TYPE_BACK),
-                deleteUpdates, insertUpdates);
-        recalculate |= resolveCycles(graph, dfa.getEdges(Edge.TYPE_FORWARD),
-                deleteUpdates, insertUpdates);
-
-        if (recalculate) {
-            dfa = recalculateDepthFirstAnalysis(graph, autoAssign);
-        }
-
-        // flush delete updates to null fks, then all rows in order, then
-        // the insert updates to set circular fk values
-        flush(deleteUpdates, psMgr);
-        Collection nodes = dfa.getSortedNodes();
-        for (Iterator itr = nodes.iterator(); itr.hasNext();)
-            psMgr.flush((RowImpl) itr.next());
-        flush(insertUpdates, psMgr);
-    }
-
-    /**
-     * Break a circular dependency caused by delete operations.
-     * If deleted row A has a ciricular fk to deleted row B, then use an update 
-     * statement to null A's fk to B before deleting B, then delete A.
-     * @param edge Edge in the dependency graph corresponding to a foreign key
-     * constraint. This dependency is broken by nullifying the foreign key.
-     * @param deleteUpdates Collection of update statements that are executed
-     * before the delete operations are flushed 
-     */
-    private void addDeleteUpdate(Edge edge, Collection deleteUpdates)
-        throws SQLException {
-        PrimaryRow row;
-        RowImpl update;
-        ForeignKey fk;
-
-        // copy where conditions into new update that nulls the fk
-        row = (PrimaryRow) edge.getTo();
-        update = new PrimaryRow(row.getTable(), Row.ACTION_UPDATE, null);
-        row.copyInto(update, true);
-        if (edge.getUserObject() instanceof ForeignKey) {
-            fk = (ForeignKey) edge.getUserObject();
-            update.setForeignKey(fk, row.getForeignKeyIO(fk), null);
-        } else
-            update.setNull((Column) edge.getUserObject());
-
-        deleteUpdates.add(update);
-    }
-
-    /**
-     * Break a circular dependency caused by insert operations.
-     * If inserted row A has a circular fk to updated/inserted row B,
-     * then null the fk in the B row object, then flush,
-     * and after flushing, use an update to set the fk back to A.
-     * @param row Row to be flushed
-     * @param edge Edge in the dependency graph corresponding to a foreign key
-     * constraint. This dependency is broken by nullifying the foreign key.
-     * @param insertUpdates Collection of update statements that are executed
-     * after the insert/update operations are flushed 
-     */
-    private void addInsertUpdate(PrimaryRow row, Edge edge,
-        Collection insertUpdates) throws SQLException {
-        RowImpl update;
-        ForeignKey fk;
-        Column col;
-
-        // copy where conditions into new update that sets the fk
-        update = new PrimaryRow(row.getTable(), Row.ACTION_UPDATE, null);
-        if (row.getAction() == Row.ACTION_INSERT) {
-            if (row.getPrimaryKey() == null)
-                throw new InternalException(_loc.get("ref-cycle"));
-            update.wherePrimaryKey(row.getPrimaryKey());
-        } else {
-            // Row.ACTION_UPDATE
-            row.copyInto(update, true);
-        }
-        if (edge.getUserObject() instanceof ForeignKey) {
-            fk = (ForeignKey) edge.getUserObject();
-            update.setForeignKey(fk, row.getForeignKeyIO(fk),
-                row.getForeignKeySet(fk));
-            row.clearForeignKey(fk);
-        } else {
-            col = (Column) edge.getUserObject();
-            update.setRelationId(col, row.getRelationIdSet(col),
-                row.getRelationIdCallback(col));
-            row.clearRelationId(col);
-        }
-
-        insertUpdates.add(update);
-    }
-
-    /**
-     * Finds a nullable foreign key by walking the dependency cycle. 
-     * Circular dependencies can be broken at this point.
-     * @param cycle Cycle in the dependency graph.
-     * @return Edge corresponding to a nullable foreign key.
-     */
-    private Edge findBreakableLink(List cycle) {
-        Edge breakableLink = null;
-        for (Iterator iter = cycle.iterator(); iter.hasNext(); ) {
-            Edge edge = (Edge) iter.next();
-            Object userObject = edge.getUserObject();
-            if (userObject instanceof ForeignKey) {
-                 if (!((ForeignKey) userObject).hasNotNullColumns()) {
-                     breakableLink = edge;
-                     break;
-                 }
-            } else if (userObject instanceof Column) {
-                if (!((Column) userObject).isNotNull()) {
-                    breakableLink = edge;
-                    break;
-                }
-            }
-        }
-        return breakableLink;
-    }
-
-    /**
-     * Re-calculates the DepthFirstSearch analysis of the graph 
-     * after some of the edges have been removed. Ensures
-     * that the dependency graph is cycle free.
-     * @param graph The graph of statements to be walked
-     * @param autoAssign Whether any of the rows in the graph have any
-     * auto-assign constraints
-     */
-    private DepthFirstAnalysis recalculateDepthFirstAnalysis(Graph graph,
-        boolean autoAssign) {
-        DepthFirstAnalysis dfa;
-        // clear previous traversal data
-        graph.clearTraversal();
-        dfa = newDepthFirstAnalysis(graph, autoAssign);
-        // make sure that the graph is non-cyclic now
-        assert (dfa.hasNoCycles()): _loc.get("graph-not-cycle-free");
-        return dfa;
-    }
-
-    /**
-     * Resolve circular dependencies by identifying and breaking
-     * a nullable foreign key.
-     * @param graph Dependency graph.
-     * @param edges Collection of edges. Each edge indicates a possible 
-     * circular dependency
-     * @param deleteUpdates Collection of update operations (nullifying 
-     * foreign keys) to be filled. These updates will be executed before 
-     * the rows in the dependency graph are flushed
-     * @param insertUpdates CCollection of update operations (nullifying 
-     * foreign keys) to be filled. These updates will be executed after 
-     * the rows in the dependency graph are flushed
-     * @return Depending on where circular dependencies are broken, the  
-     * topological order of the graph nodes has to be re-calculated.
-     */
-    private boolean resolveCycles(Graph graph, Collection edges,
-        Collection deleteUpdates, Collection insertUpdates)
-        throws SQLException {
-        boolean recalculate = false;
-        for (Iterator itr = edges.iterator(); itr.hasNext();) {
-            Edge edge = (Edge) itr.next();
-            List cycle = edge.getCycle();
-
-            if (cycle != null) {
-                // find a nullable foreign key
-                Edge breakableLink = findBreakableLink(cycle);
-                if (breakableLink == null) {
-                    throw new UserException(_loc.get("no-nullable-fk"));
-                }
-
-                // topologic node order must be re-calculated,  if the
-                // breakable link is different from the edge where
-                // the circular dependency was originally detected
-                if (edge != breakableLink) {
-                    recalculate = true;
-                }
-
-                if (!breakableLink.isRemovedFromGraph()) {
-
-                    // use a primary row update to prevent setting pk and fk values
-                    // until after flush, to get latest auto-increment values
-                    PrimaryRow row = (PrimaryRow) breakableLink.getFrom();
-                    if (row.getAction() == Row.ACTION_DELETE) {
-                        addDeleteUpdate(breakableLink, deleteUpdates);
-                    } else {
-                        addInsertUpdate(row, breakableLink, insertUpdates);
-                    }
-                    graph.removeEdge(breakableLink);
-                }
-            }
-        }
-        return recalculate;
-    }
-
-    /**
-     * Create a new {@link DepthFirstAnalysis} suitable for the given graph
-     * and auto-assign settings.
-     */
-    protected DepthFirstAnalysis newDepthFirstAnalysis(Graph graph,
-        boolean autoAssign) {
-        return new DepthFirstAnalysis(graph);
-    }
-
-    /**
-     * Flush the given collection of secondary rows.
-     */
-    protected void flush(Collection rows, PreparedStatementManager psMgr) {
-        if (rows.size() == 0)
-            return;
-
-        RowImpl row;
-        for (Iterator itr = rows.iterator(); itr.hasNext(); ) {
-            row = (RowImpl) itr.next();
-            if (row.isValid() && !row.isDependent())
-                psMgr.flush(row);
-        }
-    }
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openjpa.jdbc.kernel;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.openjpa.jdbc.meta.ClassMapping;
+import org.apache.openjpa.jdbc.schema.Column;
+import org.apache.openjpa.jdbc.schema.ForeignKey;
+import org.apache.openjpa.jdbc.schema.Table;
+import org.apache.openjpa.jdbc.sql.PrimaryRow;
+import org.apache.openjpa.jdbc.sql.Row;
+import org.apache.openjpa.jdbc.sql.RowImpl;
+import org.apache.openjpa.jdbc.sql.RowManager;
+import org.apache.openjpa.jdbc.sql.RowManagerImpl;
+import org.apache.openjpa.jdbc.sql.SQLExceptions;
+import org.apache.openjpa.kernel.OpenJPAStateManager;
+import org.apache.openjpa.lib.graph.DepthFirstAnalysis;
+import org.apache.openjpa.lib.graph.Edge;
+import org.apache.openjpa.lib.graph.Graph;
+import org.apache.openjpa.lib.util.Localizer;
+import org.apache.openjpa.util.InternalException;
+import org.apache.openjpa.util.OpenJPAException;
+import org.apache.openjpa.util.UserException;
+
+/**
+ * <p>Standard update manager, capable of foreign key constraint evaluation.</p>
+ *
+ * @since 1.0.0
+ */
+public class ConstraintUpdateManager
+    extends AbstractUpdateManager {
+
+    private static final Localizer _loc = Localizer.forPackage
+        (ConstraintUpdateManager.class);
+
+    public boolean orderDirty() {
+        return true;
+    }
+
+    protected PreparedStatementManager newPreparedStatementManager
+        (JDBCStore store, Connection conn) {
+        return new PreparedStatementManagerImpl(store, conn);
+    }
+
+    protected RowManager newRowManager() {
+        return new RowManagerImpl(false);
+    }
+
+    protected Collection flush(RowManager rowMgr,
+        PreparedStatementManager psMgr, Collection exceps) {
+        RowManagerImpl rmimpl = (RowManagerImpl) rowMgr;
+
+        // first take care of all secondary table deletes and 'all row' deletes
+        // (which are probably secondary table deletes), since no foreign
+        // keys ever rely on secondary table pks
+        flush(rmimpl.getAllRowDeletes(), psMgr);
+        flush(rmimpl.getSecondaryDeletes(), psMgr);
+
+        // now do any 'all row' updates
+        flush(rmimpl.getAllRowUpdates(), psMgr);
+
+        // analyze foreign keys
+        Collection inserts = rmimpl.getInserts();
+        Collection updates = rmimpl.getUpdates();
+        Collection deletes = rmimpl.getDeletes();
+        Graph[] graphs = new Graph[2];    // insert graph, delete graph
+        analyzeForeignKeys(inserts, updates, deletes, rmimpl, graphs);
+
+        // flush insert graph, if any
+        boolean autoAssign = rmimpl.hasAutoAssignConstraints();
+        try {
+            flushGraph(graphs[0], psMgr, autoAssign);
+        } catch (SQLException se) {
+            exceps = addException(exceps, SQLExceptions.getStore(se, dict));
+        } catch (OpenJPAException ke) {
+            exceps = addException(exceps, ke);
+        }
+
+        // flush the rest of the inserts and updates; inserts before updates
+        // because some update fks might reference pks that have to be inserted
+        flush(inserts, psMgr);
+        flush(updates, psMgr);
+
+        // flush the delete graph, if any
+        try {
+            flushGraph(graphs[1], psMgr, autoAssign);
+        } catch (SQLException se) {
+            exceps = addException(exceps, SQLExceptions.getStore(se, dict));
+        } catch (OpenJPAException ke) {
+            exceps = addException(exceps, ke);
+        }
+
+        // put the remainder of the deletes after updates because some updates
+        // may be nulling fks to rows that are going to be deleted
+        flush(deletes, psMgr);
+
+        // take care of all secondary table inserts and updates last, since
+        // they may rely on previous inserts or updates, but nothing relies
+        // on them
+        flush(rmimpl.getSecondaryUpdates(), psMgr);
+
+        // flush any left over prepared statements
+        psMgr.flush();
+        return exceps;
+    }
+
+    /**
+     * Analyze foreign key dependencies on the given rows
+     * and create an insert and a delete graph to execute.  The insert
+     * graph will be flushed before all other rows, and the delete graph will
+     * be flushed after them.
+     */
+    private void analyzeForeignKeys(Collection inserts, Collection updates,
+        Collection deletes, RowManagerImpl rowMgr, Graph[] graphs) {
+        // if there are any deletes, we have to map the insert objects on their
+        // oids so we'll be able to detect delete-then-insert-same-pk cases
+        Map insertMap = null;
+        OpenJPAStateManager sm;
+        if (!deletes.isEmpty() && !inserts.isEmpty()) {
+            insertMap = new HashMap((int) (inserts.size() * 1.33 + 1));
+            for (Iterator itr = inserts.iterator(); itr.hasNext();) {
+                sm = ((Row) itr.next()).getPrimaryKey();
+                if (sm != null && sm.getObjectId() != null)
+                    insertMap.put(sm.getObjectId(), sm);
+            }
+        }
+
+        // first construct the graph for deletes; this may expand to include
+        // inserts and updates as well if there are any inserts that rely on
+        // deletes (delete-then-insert-same-pk cases)
+        PrimaryRow row;
+        Row row2;
+        ForeignKey[] fks;
+        OpenJPAStateManager fkVal;
+        boolean ignoreUpdates = true;
+        for (Iterator itr = deletes.iterator(); itr.hasNext();) {
+            row = (PrimaryRow) itr.next();
+            if (!row.isValid())
+                continue;
+
+            row2 = getInsertRow(insertMap, rowMgr, row);
+            if (row2 != null) {
+                ignoreUpdates = false;
+                graphs[1] = addEdge(graphs[1], (PrimaryRow) row2, row, null);
+            }
+
+            // now check this row's fks against other deletes
+            fks = row.getTable().getForeignKeys();
+            for (int j = 0; j < fks.length; j++) {
+                // when deleting ref fks they'll just set a where value, so
+                // check both for fk updates (relation fks) and wheres (ref fks)
+                fkVal = row.getForeignKeySet(fks[j]);
+                if (fkVal == null)
+                    fkVal = row.getForeignKeyWhere(fks[j]);
+                if (fkVal == null)
+                    continue;
+
+                row2 = rowMgr.getRow(fks[j].getPrimaryKeyTable(),
+                    Row.ACTION_DELETE, fkVal, false);
+                if (row2 != null && row2.isValid() && row2 != row)
+                    graphs[1] = addEdge(graphs[1], (PrimaryRow) row2, row,
+                        fks[j]);
+            }
+        }
+
+        if (ignoreUpdates)
+            graphs[0] = analyzeAgainstInserts(inserts, rowMgr, graphs[0]);
+        else {
+            // put inserts *and updates* in the delete graph; they all rely
+            // on each other
+            graphs[1] = analyzeAgainstInserts(updates, rowMgr, graphs[1]);
+            graphs[1] = analyzeAgainstInserts(inserts, rowMgr, graphs[1]);
+        }
+    }
+
+    /**
+     * Check to see if there is an insert for for the same table and primary
+     * key values as the given delete row.
+     */
+    private Row getInsertRow(Map insertMap, RowManagerImpl rowMgr, Row row) {
+        if (insertMap == null)
+            return null;
+
+        OpenJPAStateManager sm = row.getPrimaryKey();
+        if (sm == null)
+            return null;
+
+        // look for a new object whose insert id is the same as this delete one
+        Object oid = sm.getObjectId();
+        OpenJPAStateManager nsm = (OpenJPAStateManager) insertMap.get(oid);
+        if (nsm == null)
+            return null;
+
+        // found new object; get its row
+        row = rowMgr.getRow(row.getTable(), Row.ACTION_INSERT, nsm, false);
+        return (row == null || row.isValid()) ? row : null;
+    }
+
+    /**
+     * Analyze the given rows against the inserts, placing dependencies
+     * in the given graph.
+     */
+    private Graph analyzeAgainstInserts(Collection rows, RowManagerImpl rowMgr,
+        Graph graph) {
+        PrimaryRow row;
+        Row row2;
+        ForeignKey[] fks;
+        Column[] cols;
+        for (Iterator itr = rows.iterator(); itr.hasNext();) {
+            row = (PrimaryRow) itr.next();
+            if (!row.isValid())
+                continue;
+
+            // check this row's fks against inserts; a logical fk to an auto-inc
+            // column is treated just as actual database fk because the result
+            // is the same: the pk row has to be inserted before the fk row
+            fks = row.getTable().getForeignKeys();
+            for (int j = 0; j < fks.length; j++) {
+                if (row.getForeignKeySet(fks[j]) == null)
+                    continue;
+
+                // see if this row is dependent on another.  if it's only
+                // depenent on itself, see if the fk is logical or deferred, in
+                // which case it must be an auto-inc because otherwise we
+                // wouldn't have recorded it
+                row2 = rowMgr.getRow(fks[j].getPrimaryKeyTable(),
+                    Row.ACTION_INSERT, row.getForeignKeySet(fks[j]), false);
+                if (row2 != null && row2.isValid() && (row2 != row
+                    || fks[j].isDeferred() || fks[j].isLogical()))
+                    graph = addEdge(graph, row, (PrimaryRow) row2, fks[j]);
+            }
+
+            // see if there are any relation id columns dependent on
+            // auto-inc objects
+            cols = row.getTable().getRelationIdColumns();
+            for (int j = 0; j < cols.length; j++) {
+                OpenJPAStateManager sm = row.getRelationIdSet(cols[j]);
+                if (sm == null)
+                    continue;
+
+                row2 = rowMgr.getRow(getBaseTable(sm), Row.ACTION_INSERT,
+                    sm, false);
+                if (row2 != null && row2.isValid())
+                    graph = addEdge(graph, row, (PrimaryRow) row2, cols[j]);
+            }
+        }
+        return graph;
+    }
+
+    /**
+     * Return the base table for the given instance.
+     */
+    private static Table getBaseTable(OpenJPAStateManager sm) {
+        ClassMapping cls = (ClassMapping) sm.getMetaData();
+        while (cls.getJoinablePCSuperclassMapping() != null)
+            cls = cls.getJoinablePCSuperclassMapping();
+        return cls.getTable();
+    }
+
+    /**
+     * Add an edge between the given rows in the given foreign key graph.
+     */
+    private Graph addEdge(Graph graph, PrimaryRow row1, PrimaryRow row2,
+        Object fk) {
+        // delay creation of the graph
+        if (graph == null)
+            graph = new Graph();
+
+        row1.setDependent(true);
+        row2.setDependent(true);
+        graph.addNode(row1);
+        graph.addNode(row2);
+
+        // add an edge from row1 to row2, and set the fk causing the
+        // dependency as the user object so we can retrieve it when resolving
+        // circular constraints
+        Edge edge = new Edge(row1, row2, true);
+        edge.setUserObject(fk);
+        graph.addEdge(edge);
+
+        return graph;
+    }
+
+    /**
+     * Flush the given graph of rows in the proper order.
+     * @param graph The graph of statements to be walked
+     * @param psMgr The prepared statement manager to use to issue the
+     * statements
+     * @param autoAssign Whether any of the rows in the graph have any
+     * auto-assign constraints
+     */
+    protected void flushGraph(Graph graph, PreparedStatementManager psMgr,
+        boolean autoAssign)
+        throws SQLException {
+        if (graph == null)
+            return;
+
+        DepthFirstAnalysis dfa = newDepthFirstAnalysis(graph, autoAssign);
+        Collection insertUpdates = new LinkedList();
+        Collection deleteUpdates = new LinkedList();
+        boolean recalculate;
+
+        // Handle circular constraints:
+        // - if deleted row A has a ciricular fk to deleted row B, 
+        //   then use an update statement to null A's fk to B before flushing, 
+        //   and then flush
+        // - if inserted row A has a circular fk to updated/inserted row B,
+        //   then null the fk in the B row object, then flush,
+        //   and after flushing, use an update to set the fk back to A
+        // Depending on where circular dependencies are broken, the  
+        // topological order of the graph nodes has to be re-calculated.
+        recalculate = resolveCycles(graph, dfa.getEdges(Edge.TYPE_BACK),
+                deleteUpdates, insertUpdates);
+        recalculate |= resolveCycles(graph, dfa.getEdges(Edge.TYPE_FORWARD),
+                deleteUpdates, insertUpdates);
+
+        if (recalculate) {
+            dfa = recalculateDepthFirstAnalysis(graph, autoAssign);
+        }
+
+        // flush delete updates to null fks, then all rows in order, then
+        // the insert updates to set circular fk values
+        flush(deleteUpdates, psMgr);
+        Collection nodes = dfa.getSortedNodes();
+        for (Iterator itr = nodes.iterator(); itr.hasNext();)
+            psMgr.flush((RowImpl) itr.next());
+        flush(insertUpdates, psMgr);
+    }
+
+    /**
+     * Break a circular dependency caused by delete operations.
+     * If deleted row A has a ciricular fk to deleted row B, then use an update 
+     * statement to null A's fk to B before deleting B, then delete A.
+     * @param edge Edge in the dependency graph corresponding to a foreign key
+     * constraint. This dependency is broken by nullifying the foreign key.
+     * @param deleteUpdates Collection of update statements that are executed
+     * before the delete operations are flushed 
+     */
+    private void addDeleteUpdate(Edge edge, Collection deleteUpdates)
+        throws SQLException {
+        PrimaryRow row;
+        RowImpl update;
+        ForeignKey fk;
+
+        // copy where conditions into new update that nulls the fk
+        row = (PrimaryRow) edge.getTo();
+        update = new PrimaryRow(row.getTable(), Row.ACTION_UPDATE, null);
+        row.copyInto(update, true);
+        if (edge.getUserObject() instanceof ForeignKey) {
+            fk = (ForeignKey) edge.getUserObject();
+            update.setForeignKey(fk, row.getForeignKeyIO(fk), null);
+        } else
+            update.setNull((Column) edge.getUserObject());
+
+        deleteUpdates.add(update);
+    }
+
+    /**
+     * Break a circular dependency caused by insert operations.
+     * If inserted row A has a circular fk to updated/inserted row B,
+     * then null the fk in the B row object, then flush,
+     * and after flushing, use an update to set the fk back to A.
+     * @param row Row to be flushed
+     * @param edge Edge in the dependency graph corresponding to a foreign key
+     * constraint. This dependency is broken by nullifying the foreign key.
+     * @param insertUpdates Collection of update statements that are executed
+     * after the insert/update operations are flushed 
+     */
+    private void addInsertUpdate(PrimaryRow row, Edge edge,
+        Collection insertUpdates) throws SQLException {
+        RowImpl update;
+        ForeignKey fk;
+        Column col;
+
+        // copy where conditions into new update that sets the fk
+        update = new PrimaryRow(row.getTable(), Row.ACTION_UPDATE, null);
+        if (row.getAction() == Row.ACTION_INSERT) {
+            if (row.getPrimaryKey() == null)
+                throw new InternalException(_loc.get("ref-cycle"));
+            update.wherePrimaryKey(row.getPrimaryKey());
+        } else {
+            // Row.ACTION_UPDATE
+            row.copyInto(update, true);
+        }
+        if (edge.getUserObject() instanceof ForeignKey) {
+            fk = (ForeignKey) edge.getUserObject();
+            update.setForeignKey(fk, row.getForeignKeyIO(fk),
+                row.getForeignKeySet(fk));
+            row.clearForeignKey(fk);
+        } else {
+            col = (Column) edge.getUserObject();
+            update.setRelationId(col, row.getRelationIdSet(col),
+                row.getRelationIdCallback(col));
+            row.clearRelationId(col);
+        }
+
+        insertUpdates.add(update);
+    }
+
+    /**
+     * Finds a nullable foreign key by walking the dependency cycle. 
+     * Circular dependencies can be broken at this point.
+     * @param cycle Cycle in the dependency graph.
+     * @return Edge corresponding to a nullable foreign key.
+     */
+    private Edge findBreakableLink(List cycle) {
+        Edge breakableLink = null;
+        for (Iterator iter = cycle.iterator(); iter.hasNext(); ) {
+            Edge edge = (Edge) iter.next();
+            Object userObject = edge.getUserObject();
+            if (userObject instanceof ForeignKey) {
+                 if (!((ForeignKey) userObject).hasNotNullColumns()) {
+                     breakableLink = edge;
+                     break;
+                 }
+            } else if (userObject instanceof Column) {
+                if (!((Column) userObject).isNotNull()) {
+                    breakableLink = edge;
+                    break;
+                }
+            }
+        }
+        return breakableLink;
+    }
+
+    /**
+     * Re-calculates the DepthFirstSearch analysis of the graph 
+     * after some of the edges have been removed. Ensures
+     * that the dependency graph is cycle free.
+     * @param graph The graph of statements to be walked
+     * @param autoAssign Whether any of the rows in the graph have any
+     * auto-assign constraints
+     */
+    private DepthFirstAnalysis recalculateDepthFirstAnalysis(Graph graph,
+        boolean autoAssign) {
+        DepthFirstAnalysis dfa;
+        // clear previous traversal data
+        graph.clearTraversal();
+        dfa = newDepthFirstAnalysis(graph, autoAssign);
+        // make sure that the graph is non-cyclic now
+        assert (dfa.hasNoCycles()): _loc.get("graph-not-cycle-free");
+        return dfa;
+    }
+
+    /**
+     * Resolve circular dependencies by identifying and breaking
+     * a nullable foreign key.
+     * @param graph Dependency graph.
+     * @param edges Collection of edges. Each edge indicates a possible 
+     * circular dependency
+     * @param deleteUpdates Collection of update operations (nullifying 
+     * foreign keys) to be filled. These updates will be executed before 
+     * the rows in the dependency graph are flushed
+     * @param insertUpdates CCollection of update operations (nullifying 
+     * foreign keys) to be filled. These updates will be executed after 
+     * the rows in the dependency graph are flushed
+     * @return Depending on where circular dependencies are broken, the  
+     * topological order of the graph nodes has to be re-calculated.
+     */
+    private boolean resolveCycles(Graph graph, Collection edges,
+        Collection deleteUpdates, Collection insertUpdates)
+        throws SQLException {
+        boolean recalculate = false;
+        for (Iterator itr = edges.iterator(); itr.hasNext();) {
+            Edge edge = (Edge) itr.next();
+            List cycle = edge.getCycle();
+
+            if (cycle != null) {
+                // find a nullable foreign key
+                Edge breakableLink = findBreakableLink(cycle);
+                if (breakableLink == null) {
+                    throw new UserException(_loc.get("no-nullable-fk"));
+                }
+
+                // topologic node order must be re-calculated,  if the
+                // breakable link is different from the edge where
+                // the circular dependency was originally detected
+                if (edge != breakableLink) {
+                    recalculate = true;
+                }
+
+                if (!breakableLink.isRemovedFromGraph()) {
+
+                    // use a primary row update to prevent setting pk and fk values
+                    // until after flush, to get latest auto-increment values
+                    PrimaryRow row = (PrimaryRow) breakableLink.getFrom();
+                    if (row.getAction() == Row.ACTION_DELETE) {
+                        addDeleteUpdate(breakableLink, deleteUpdates);
+                    } else {
+                        addInsertUpdate(row, breakableLink, insertUpdates);
+                    }
+                    graph.removeEdge(breakableLink);
+                }
+            }
+        }
+        return recalculate;
+    }
+
+    /**
+     * Create a new {@link DepthFirstAnalysis} suitable for the given graph
+     * and auto-assign settings.
+     */
+    protected DepthFirstAnalysis newDepthFirstAnalysis(Graph graph,
+        boolean autoAssign) {
+        return new DepthFirstAnalysis(graph);
+    }
+
+    /**
+     * Flush the given collection of secondary rows.
+     */
+    protected void flush(Collection rows, PreparedStatementManager psMgr) {
+        if (rows.size() == 0)
+            return;
+
+        RowImpl row;
+        for (Iterator itr = rows.iterator(); itr.hasNext(); ) {
+            row = (RowImpl) itr.next();
+            if (row.isValid() && !row.isDependent())
+                psMgr.flush(row);
+        }
+    }
 }
\ No newline at end of file

Propchange: openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/ConstraintUpdateManager.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/RowManagerImpl.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/RowManagerImpl.java?rev=889793&r1=889792&r2=889793&view=diff
==============================================================================
--- openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/RowManagerImpl.java (original)
+++ openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/RowManagerImpl.java Fri Dec 11 20:06:18 2009
@@ -22,7 +22,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -39,13 +39,13 @@
 public class RowManagerImpl
     implements RowManager {
 
-    private Map _inserts = null;
-    private Map _updates = null;
-    private Map _deletes = null;
-    private Collection _secondaryUpdates = null;
-    private Collection _secondaryDeletes = null;
-    private Collection _allRowUpdates = null;
-    private Collection _allRowDeletes = null;
+ private Map<Key, PrimaryRow> _inserts = null;
+ private Map<Key, PrimaryRow> _updates = null;
+ private Map<Key, PrimaryRow> _deletes = null;
+ private Collection<SecondaryRow> _secondaryUpdates = null;
+ private Collection<SecondaryRow> _secondaryDeletes = null;
+ private Collection<Row> _allRowUpdates = null;
+ private Collection<Row> _allRowDeletes = null;
 
     // we maintain a list of the order of all primary rows if the user
     // wants to be able to fetch them in order
@@ -66,7 +66,7 @@
      * @param order whether to keep track of the order in which rows are added
      */
     public RowManagerImpl(boolean order) {
-        _primaryOrder = (order) ? new ArrayList() : null;
+     _primaryOrder = (order) ? new ArrayList<PrimaryRow>() : null;
     }
 
     /**
@@ -80,61 +80,98 @@
      * Return the ordered primary rows. Only available if ordering requested
      * on construction.
      */
-    public List getOrdered() {
-        return (_primaryOrder == null) ? Collections.EMPTY_LIST : _primaryOrder;
+    public List<PrimaryRow> getOrdered() {
+        if(_primaryOrder == null ) { 
+            return Collections.emptyList();
+        }
+        else { 
+            return _primaryOrder;
+        }
     }
 
     /**
      * Return all inserted primary rows.
      */
-    public Collection getInserts() {
-        return (_inserts == null) ? Collections.EMPTY_LIST : _inserts.values();
+    public Collection<PrimaryRow> getInserts() {
+        if(_inserts == null ) {
+             return Collections.emptyList();
+        }
+        else {
+            return _inserts.values();
+        }
     }
 
     /**
      * Return all updated primary rows.
      */
-    public Collection getUpdates() {
-        return (_updates == null) ? Collections.EMPTY_LIST : _updates.values();
+    public Collection<PrimaryRow> getUpdates() {
+        if(_updates == null ){ 
+            return Collections.emptyList();
+        }
+        else { 
+            return _updates.values();
+        }
     }
 
     /**
      * Return all deleted primary rows.
      */
-    public Collection getDeletes() {
-        return (_deletes == null) ? Collections.EMPTY_LIST : _deletes.values();
+    public Collection<PrimaryRow> getDeletes() {
+        if(_deletes == null) { 
+            return Collections.emptyList();
+        }
+        else {
+            return _deletes.values();
+        }
     }
 
     /**
      * Return all inserted and updated secondary rows.
      */
-    public Collection getSecondaryUpdates() {
-        return (_secondaryUpdates == null) ? Collections.EMPTY_LIST
-            : _secondaryUpdates;
+    public Collection<SecondaryRow> getSecondaryUpdates() {
+        if(_secondaryUpdates == null) { 
+            return Collections.emptyList();
+        }
+        else { 
+            return _secondaryUpdates;
+        }
     }
 
     /**
      * Return all deleted secondary rows.
      */
-    public Collection getSecondaryDeletes() {
-        return (_secondaryDeletes == null) ? Collections.EMPTY_LIST
-            : _secondaryDeletes;
+    public Collection<SecondaryRow> getSecondaryDeletes() {
+        if(_secondaryDeletes == null) { 
+            return Collections.emptyList();
+        }
+        else { 
+            return _secondaryDeletes;
+        }
     }
 
     /**
      * Return any 'all row' updates.
      */
-    public Collection getAllRowUpdates() {
-        return (_allRowUpdates == null) ? Collections.EMPTY_LIST
-            : _allRowUpdates;
+    public Collection<Row> getAllRowUpdates() {
+        if(_allRowUpdates == null) { 
+            return Collections.emptyList();
+        }
+        else { 
+            return _allRowUpdates;
+        }
     }
 
     /**
      * Return any 'all row' deletes.
      */
-    public Collection getAllRowDeletes() {
-        return (_allRowDeletes == null) ? Collections.EMPTY_LIST
-            : _allRowDeletes;
+    public Collection<Row> getAllRowDeletes() {
+        if(_allRowDeletes == null) { 
+            return Collections.emptyList();
+        }
+        else { 
+            return _allRowDeletes;
+        }    
+
     }
 
     public Row getSecondaryRow(Table table, int action) {
@@ -149,12 +186,12 @@
         SecondaryRow srow = (SecondaryRow) row;
         if (srow.getAction() == Row.ACTION_DELETE) {
             if (_secondaryDeletes == null)
-                _secondaryDeletes = new ArrayList();
-            _secondaryDeletes.add(srow.clone());
+             _secondaryDeletes = new ArrayList<SecondaryRow>();
+            _secondaryDeletes.add((SecondaryRow) srow.clone());
         } else {
             if (_secondaryUpdates == null)
-                _secondaryUpdates = new ArrayList();
-            _secondaryUpdates.add(srow.clone());
+             _secondaryUpdates = new ArrayList<SecondaryRow>();
+            _secondaryUpdates.add((SecondaryRow) srow.clone());
         }
     }
 
@@ -169,12 +206,12 @@
         switch (row.getAction()) {
             case Row.ACTION_UPDATE:
                 if (_allRowUpdates == null)
-                    _allRowUpdates = new ArrayList();
+                 _allRowUpdates = new ArrayList<Row>();
                 _allRowUpdates.add(row);
                 break;
             case Row.ACTION_DELETE:
                 if (_allRowDeletes == null)
-                    _allRowDeletes = new ArrayList();
+                 _allRowDeletes = new ArrayList<Row>();
                 _allRowDeletes.add(row);
                 break;
             default:
@@ -192,25 +229,25 @@
             && _row != null && _row.getAction() == action)
             return _row;
 
-        Map map;
+        Map<Key, PrimaryRow> map;
         if (action == Row.ACTION_DELETE) {
             if (_deletes == null && create)
-                _deletes = new HashMap();
+             _deletes = new LinkedHashMap<Key, PrimaryRow>();
             map = _deletes;
         } else if (action == Row.ACTION_INSERT) {
             if (_inserts == null && create)
-                _inserts = new HashMap();
+             _inserts = new LinkedHashMap<Key, PrimaryRow>();
             map = _inserts;
         } else {
             if (_updates == null && create)
-                _updates = new HashMap();
+             _updates = new LinkedHashMap<Key, PrimaryRow>();
             map = _updates;
         }
         if (map == null)
             return null;
 
         _key = new Key(table, sm);
-        _row = (PrimaryRow) map.get(_key);
+        _row = map.get(_key);
 
         if (_row == null && create) {
             _row = new PrimaryRow(table, action, sm);

Added: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Employee.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Employee.java?rev=889793&view=auto
==============================================================================
--- openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Employee.java (added)
+++ openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Employee.java Fri Dec 11 20:06:18 2009
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openjpa.jdbc.kernel;
+
+import java.util.Collection;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+import javax.persistence.Version;
+
+@Entity
+// Try not to collide with other Employee entities
+@Table(name = "PER_JDBC_KERN_EMP")
+public class Employee {
+
+    @Id
+    private int id;
+
+    @Version
+    private int version;
+
+    private String firstName;
+    private String lastName;
+
+    @OneToMany(mappedBy = "employee", cascade = { CascadeType.MERGE,
+        CascadeType.PERSIST })     
+    private Collection<Task> tasks;
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public String getFirstName() {
+        return firstName;
+    }
+
+    public void setFirstName(String firstName) {
+        this.firstName = firstName;
+    }
+
+    public String getLastName() {
+        return lastName;
+    }
+
+    public void setLastName(String lastName) {
+        this.lastName = lastName;
+    }
+
+    public Collection<Task> getTasks() {
+        return tasks;
+    }
+
+    public void setTasks(Collection<Task> tasks) {
+        this.tasks = tasks;
+    }
+
+    public int getVersion() {
+        return version;
+    }
+}

Propchange: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Employee.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Story.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Story.java?rev=889793&view=auto
==============================================================================
--- openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Story.java (added)
+++ openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Story.java Fri Dec 11 20:06:18 2009
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openjpa.jdbc.kernel;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import javax.persistence.Version;
+
+@Entity
+@Table(name = "PER_JDBC_KERN_STORY") // try not to collide
+public class Story {
+    @Id
+    private int id;
+
+    @Version
+    private int version;
+    
+    @ManyToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST })
+    private Task task;
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public Task getTask() {
+        return task;
+    }
+
+    public void setTask(Task task) {
+        this.task = task;
+    }
+
+    public int getVersion() {
+        return version;
+    } 
+
+}

Propchange: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Story.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Task.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Task.java?rev=889793&view=auto
==============================================================================
--- openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Task.java (added)
+++ openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Task.java Fri Dec 11 20:06:18 2009
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openjpa.jdbc.kernel;
+
+import java.util.Collection;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+import javax.persistence.Version;
+
+@Entity
+@Table(name = "PER_JDBC_KERN_TASK")
+// try not to collide
+public class Task {
+    @Id
+    private int id;
+
+    @Version
+    private int version;
+
+    @OneToMany(mappedBy = "task", cascade = { CascadeType.MERGE,
+        CascadeType.PERSIST })
+    private Collection<Story> stories;
+    
+    @ManyToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST })
+    private Employee employee;
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public Collection<Story> getStories() {
+        return stories;
+    }
+
+    public void setStories(Collection<Story> stories) {
+        this.stories = stories;
+    }
+
+    public Employee getEmployee() {
+        return employee;
+    }
+
+    public void setEmployee(Employee employee) {
+        this.employee = employee;
+    }
+
+    public int getVersion() {
+        return version;
+    } 
+}

Propchange: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Task.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/TestInsertOrder.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/TestInsertOrder.java?rev=889793&view=auto
==============================================================================
--- openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/TestInsertOrder.java (added)
+++ openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/TestInsertOrder.java Fri Dec 11 20:06:18 2009
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openjpa.jdbc.kernel;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import javax.persistence.EntityManager;
+
+import org.apache.openjpa.persistence.test.SQLListenerTestCase;
+
+/**
+ * Test that insert order is preserved when using the ConstraintUpdateManager
+ * for entities which are not annotated with ForeignKey constraints.
+ */
+public class TestInsertOrder extends SQLListenerTestCase {
+    private String empTableName;
+    private String taskTableName;
+    private String storyTableName;
+
+    public void setUp() {
+        setUp(Employee.class, Task.class, Story.class);
+        empTableName = getMapping(Employee.class).getTable().getFullName();
+        taskTableName = getMapping(Task.class).getTable().getFullName();
+        storyTableName = getMapping(Story.class).getTable().getFullName();
+    }
+
+    /**
+     * <P>Persist an Employee entity and allow the cascade to insert the
+     * children. The inserts should be executed in this order, Employee, Task,
+     * Story.
+     * </P>
+     * 
+     * <P> 
+     * Originally this test would pass in some scenarios. I believe the order 
+     * relied on the hashcode of the underlying entities. 
+     * </P>
+     */
+    public void testCascadePersist() {
+        Employee e = newTree(10);
+        EntityManager em = emf.createEntityManager();
+        em.getTransaction().begin();
+        em.persist(e);
+        em.getTransaction().commit();
+        em.close();
+
+        assertAllSQLInOrder("INSERT INTO " + empTableName + ".*", "INSERT INTO "
+            + taskTableName + ".*", "INSERT INTO " + storyTableName + ".*");
+    }
+    
+    /**
+     * Merge an Employee entity and allow the cascade to insert the children.
+     * The inserts should be executed in this order, Employee, Task, Story.
+     */
+    public void testCascadeMerge() {
+        Employee e = newTree(11);
+        EntityManager em = emf.createEntityManager();
+        em.getTransaction().begin();
+        em.merge(e);
+        em.getTransaction().commit();
+        em.close();
+
+        assertAllSQLInOrder("INSERT INTO " + empTableName + ".*", "INSERT INTO "
+            + taskTableName + ".*", "INSERT INTO " + storyTableName + ".*");
+    }
+
+
+    /**
+     * Helper to create a tree of entities
+     * 
+     * @param id
+     *            ID for the entities.
+     * @return an unmanaged Employee instance with the appropriate relationships
+     *         set.
+     */
+    private Employee newTree(int id) {
+        Employee e = new Employee();
+        e.setId(id);
+
+        Task t = new Task();
+        t.setId(id);
+
+        Story s = new Story();
+        s.setId(id);
+
+        Collection<Task> tasks = new ArrayList<Task>();
+        tasks.add(t);
+
+        Collection<Story> stories = new ArrayList<Story>();
+        stories.add(s);
+
+        e.setTasks(tasks);
+        t.setEmployee(e);
+
+        t.setStories(stories);
+        s.setTask(t);
+
+        return e;
+    }
+}

Propchange: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/TestInsertOrder.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/test/SQLListenerTestCase.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/test/SQLListenerTestCase.java?rev=889793&r1=889792&r2=889793&view=diff
==============================================================================
--- openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/test/SQLListenerTestCase.java (original)
+++ openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/test/SQLListenerTestCase.java Fri Dec 11 20:06:18 2009
@@ -20,7 +20,6 @@
 
 import java.util.List;
 import java.util.ArrayList;
-import java.util.Map;
 
 import org.apache.openjpa.lib.jdbc.AbstractJDBCListener;
 import org.apache.openjpa.lib.jdbc.JDBCEvent;
@@ -34,6 +33,7 @@
 public abstract class SQLListenerTestCase
     extends SingleEMFTestCase {
 
+    private static String _nl = System.getProperty("line.separator");
     protected List<String> sql = new ArrayList<String>();
     protected int sqlCount;
     
@@ -95,6 +95,47 @@
     }
     
     /**
+     * Confirm the list of expected SQL expressions have been executed in the
+     * order specified. I.e. additional SQL statements can be executed in
+     * between expected SQLs.
+     * 
+     * @param expected
+     *            SQL expressions. E.g., ("SELECT FOO .*", "UPDATE .*")
+     */
+    public void assertAllSQLInOrder(String... expected) {
+        assertSQLInOrder(false, expected);
+    }
+    
+    private void assertSQLInOrder(boolean exact, String... expected) {
+        boolean match = false;
+        int sqlSize = sql.size();
+        if (expected.length <= sqlSize) {
+            int hits = 0;
+            for (String executedSQL : sql) {
+                if (executedSQL.matches(expected[hits])) {
+                    if (++hits == expected.length)
+                        break;
+                }
+            }
+            match = hits == (exact ? sqlSize : expected.length);
+        }
+
+        if (!match) {
+            StringBuilder sb = new StringBuilder();
+            sb.append("Did not find SQL in expected order : ").append(_nl);
+            for (String s : expected) {
+                sb.append(s).append(_nl);
+            }
+
+            sb.append("SQL Statements issued : ");
+            for (String s : sql) {
+                sb.append(s).append(_nl);
+            }
+            fail(sb.toString());
+        }
+    }
+    
+    /**
      * Gets the number of SQL issued since last reset.
      */
     public int getSQLCount() {
@@ -122,4 +163,28 @@
             }
 		}
 	}
+    
+    public void assertSQLOrder(String... expected) {
+        int hits = 0;
+    
+        for (String executedSQL : sql) {
+            if (executedSQL.matches(expected[hits])) {
+                hits++;
+            }
+        }
+    
+        if (hits != expected.length) {
+            StringBuilder sb = new StringBuilder();
+            sb.append("Did not find SQL in expected order : ").append(_nl);
+            for (String s : expected) {
+                sb.append(s).append(_nl);
+            }
+    
+            sb.append("SQL Statements issued : ");
+            for (String s : sql) {
+                sb.append(s).append(_nl);
+            }
+            fail(sb.toString());
+        }
+    }
 }