diff options
9 files changed, 984 insertions, 381 deletions
diff --git a/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/db/CDODBUtil.java b/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/db/CDODBUtil.java index b91f504011..1fe225cb17 100644 --- a/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/db/CDODBUtil.java +++ b/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/db/CDODBUtil.java @@ -60,6 +60,11 @@ public final class CDODBUtil */ public static final String PROP_COPY_ON_BRANCH = "copyOnBranch"; + /** + * @since 4.1 + */ + public static final String PROP_ZEROBASED_INDEX = "forceZeroBasedIndex"; + private CDODBUtil() { } diff --git a/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/db/mapping/IListMapping2.java b/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/db/mapping/IListMapping2.java new file mode 100644 index 0000000000..9dcfd6cbc9 --- /dev/null +++ b/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/db/mapping/IListMapping2.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2004 - 2011 Eike Stepper (Berlin, Germany) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Eike Stepper - initial API and implementation + */ +package org.eclipse.emf.cdo.server.db.mapping; + +import org.eclipse.emf.cdo.common.id.CDOID; +import org.eclipse.emf.cdo.server.db.IDBStoreAccessor; + +/** + * Extension interface to {@link IListMapping}. + * + * @author Eike Stepper + * @since 4.1 + */ +public interface IListMapping2 extends IListMapping +{ + public void addSimpleChunkWhere(IDBStoreAccessor accessor, CDOID cdoid, StringBuilder builder, int index); + + public void addRangedChunkWhere(IDBStoreAccessor accessor, CDOID cdoid, StringBuilder builder, int fromIndex, + int toIndex); +} diff --git a/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/DBStoreChunkReader.java b/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/DBStoreChunkReader.java index b98c95fc73..2295a59bb7 100644 --- a/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/DBStoreChunkReader.java +++ b/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/DBStoreChunkReader.java @@ -16,6 +16,7 @@ import org.eclipse.emf.cdo.common.revision.CDORevision; import org.eclipse.emf.cdo.server.db.IDBStoreChunkReader; import org.eclipse.emf.cdo.server.db.mapping.IClassMapping; import org.eclipse.emf.cdo.server.db.mapping.IListMapping; +import org.eclipse.emf.cdo.server.db.mapping.IListMapping2; import org.eclipse.emf.cdo.server.db.mapping.IMappingStrategy; import org.eclipse.emf.cdo.spi.server.StoreChunkReader; @@ -52,9 +53,16 @@ public class DBStoreChunkReader extends StoreChunkReader implements IDBStoreChun super.addSimpleChunk(index); prepareAddition(); - builder.append(CDODBSchema.LIST_IDX); - builder.append('='); - builder.append(index); + if (referenceMapping instanceof IListMapping2) + { + ((IListMapping2)referenceMapping).addSimpleChunkWhere(getAccessor(), getRevision().getID(), builder, index); + } + else + { + builder.append(CDODBSchema.LIST_IDX); + builder.append('='); + builder.append(index); + } } @Override @@ -63,11 +71,19 @@ public class DBStoreChunkReader extends StoreChunkReader implements IDBStoreChun super.addRangedChunk(fromIndex, toIndex); prepareAddition(); - builder.append(CDODBSchema.LIST_IDX); - builder.append(" BETWEEN "); //$NON-NLS-1$ - builder.append(fromIndex); - builder.append(" AND "); //$NON-NLS-1$ - builder.append(toIndex - 1); + if (referenceMapping instanceof IListMapping2) + { + ((IListMapping2)referenceMapping).addRangedChunkWhere(getAccessor(), getRevision().getID(), builder, fromIndex, + toIndex); + } + else + { + builder.append(CDODBSchema.LIST_IDX); + builder.append(" BETWEEN "); //$NON-NLS-1$ + builder.append(fromIndex); + builder.append(" AND "); //$NON-NLS-1$ + builder.append(toIndex - 1); + } } public List<Chunk> executeRead() diff --git a/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/mapping/horizontal/BasicAbstractListTableMapping.java b/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/mapping/horizontal/BasicAbstractListTableMapping.java index 9fccee2e47..f5c54477e1 100644 --- a/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/mapping/horizontal/BasicAbstractListTableMapping.java +++ b/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/mapping/horizontal/BasicAbstractListTableMapping.java @@ -10,8 +10,11 @@ */ package org.eclipse.emf.cdo.server.internal.db.mapping.horizontal; -import org.eclipse.emf.cdo.server.db.mapping.IListMapping; +import org.eclipse.emf.cdo.common.id.CDOID; +import org.eclipse.emf.cdo.server.db.IDBStoreAccessor; +import org.eclipse.emf.cdo.server.db.mapping.IListMapping2; import org.eclipse.emf.cdo.server.db.mapping.IMappingStrategy; +import org.eclipse.emf.cdo.server.internal.db.CDODBSchema; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EStructuralFeature; @@ -19,7 +22,7 @@ import org.eclipse.emf.ecore.EStructuralFeature; /** * @author Stefan Winkler */ -public abstract class BasicAbstractListTableMapping implements IListMapping +public abstract class BasicAbstractListTableMapping implements IListMapping2 { private IMappingStrategy mappingStrategy; @@ -49,4 +52,22 @@ public abstract class BasicAbstractListTableMapping implements IListMapping { return feature; } + + public void addSimpleChunkWhere(IDBStoreAccessor accessor, CDOID cdoid, StringBuilder builder, int index) + { + builder.append(CDODBSchema.LIST_IDX); + builder.append('='); + builder.append(index); + } + + public void addRangedChunkWhere(IDBStoreAccessor accessor, CDOID cdoid, StringBuilder builder, int fromIndex, + int toIndex) + { + builder.append(CDODBSchema.LIST_IDX); + builder.append(" BETWEEN "); //$NON-NLS-1$ + builder.append(fromIndex); + builder.append(" AND "); //$NON-NLS-1$ + builder.append(toIndex - 1); + } + } diff --git a/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/mapping/horizontal/HorizontalNonAuditMappingStrategy.java b/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/mapping/horizontal/HorizontalNonAuditMappingStrategy.java index b317c16cdd..86beda1121 100644 --- a/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/mapping/horizontal/HorizontalNonAuditMappingStrategy.java +++ b/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/mapping/horizontal/HorizontalNonAuditMappingStrategy.java @@ -11,6 +11,7 @@ */ package org.eclipse.emf.cdo.server.internal.db.mapping.horizontal; +import org.eclipse.emf.cdo.server.db.CDODBUtil; import org.eclipse.emf.cdo.server.db.mapping.IClassMapping; import org.eclipse.emf.cdo.server.db.mapping.IListMapping; @@ -23,6 +24,8 @@ import org.eclipse.emf.ecore.EStructuralFeature; */ public class HorizontalNonAuditMappingStrategy extends AbstractHorizontalMappingStrategy { + private boolean forceZeroBasedIndex; + public HorizontalNonAuditMappingStrategy() { } @@ -42,6 +45,11 @@ public class HorizontalNonAuditMappingStrategy extends AbstractHorizontalMapping return true; } + public boolean shallForceZeroBasedIndex() + { + return forceZeroBasedIndex; + } + @Override public IListMapping doCreateListMapping(EClass containingClass, EStructuralFeature feature) { @@ -59,4 +67,13 @@ public class HorizontalNonAuditMappingStrategy extends AbstractHorizontalMapping { return new HorizontalNonAuditClassMapping(this, eClass); } + + @Override + protected void doAfterActivate() throws Exception + { + super.doAfterActivate(); + + String value = getProperties().get(CDODBUtil.PROP_ZEROBASED_INDEX); + forceZeroBasedIndex = value == null ? false : Boolean.valueOf(value); + } } diff --git a/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/mapping/horizontal/NonAuditListTableMapping.java b/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/mapping/horizontal/NonAuditListTableMapping.java index 18455f145c..5a28077f51 100644 --- a/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/mapping/horizontal/NonAuditListTableMapping.java +++ b/plugins/org.eclipse.emf.cdo.server.db/src/org/eclipse/emf/cdo/server/internal/db/mapping/horizontal/NonAuditListTableMapping.java @@ -27,6 +27,7 @@ import org.eclipse.emf.cdo.common.revision.delta.CDOSetFeatureDelta; import org.eclipse.emf.cdo.common.revision.delta.CDOUnsetFeatureDelta; import org.eclipse.emf.cdo.server.db.IDBStoreAccessor; import org.eclipse.emf.cdo.server.db.IIDHandler; +import org.eclipse.emf.cdo.server.db.IPreparedStatementCache; import org.eclipse.emf.cdo.server.db.IPreparedStatementCache.ReuseProbability; import org.eclipse.emf.cdo.server.db.mapping.IListMappingDeltaSupport; import org.eclipse.emf.cdo.server.db.mapping.IMappingStrategy; @@ -45,8 +46,12 @@ import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.core.runtime.Assert; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.ListIterator; /** * This is a list-to-table mapping optimized for non-audit-mode. It doesn't care about version and has delta support. @@ -62,17 +67,6 @@ public class NonAuditListTableMapping extends AbstractListTableMapping implement private static final int UNBOUNDED_SHIFT = -1; - /** - * The central data structure which is used to calculate the outcomes of the list deltas. - */ - private ArrayList<ManipulationElement> manipulations = new ArrayList<ManipulationElement>(); - - /** - * This is a flag to remember if a delta of type "clear" has been encountered. If so, the list in the DB has to be - * cleared before writing out the changes. - */ - private boolean clearFirst; - private String sqlClear; private String sqlUpdateValue; @@ -83,6 +77,12 @@ public class NonAuditListTableMapping extends AbstractListTableMapping implement private String sqlDeleteItem; + private String sqlShiftDownIndex; + + private String sqlReadCurrentIndexOffset; + + private String sqlShiftUpIndex; + public NonAuditListTableMapping(IMappingStrategy mappingStrategy, EClass eClass, EStructuralFeature feature) { super(mappingStrategy, eClass, feature); @@ -148,6 +148,54 @@ public class NonAuditListTableMapping extends AbstractListTableMapping implement builder.append(CDODBSchema.LIST_IDX); builder.append("=? "); //$NON-NLS-1$ sqlUpdateIndex = builder.toString(); + + // ----------- mass update item indexes -------------- + builder = new StringBuilder(); + builder.append("UPDATE "); //$NON-NLS-1$ + builder.append(getTable()); + builder.append(" SET "); //$NON-NLS-1$ + builder.append(CDODBSchema.LIST_IDX); + builder.append("="); //$NON-NLS-1$ + builder.append(CDODBSchema.LIST_IDX); + builder.append("+? WHERE "); //$NON-NLS-1$ + builder.append(CDODBSchema.LIST_REVISION_ID); + builder.append("=? AND "); //$NON-NLS-1$ + builder.append(CDODBSchema.LIST_IDX); + builder.append(" BETWEEN ? AND ?"); //$NON-NLS-1$ + // getMappingStrategy().getStore().getDBAdapter() + + // needed because of MySQL: + builder.append("/*! ORDER BY "); //$NON-NLS-1$ / + builder.append(CDODBSchema.LIST_IDX); + sqlShiftDownIndex = builder.toString() + " */"; //$NON-NLS-1$ + builder.append(" DESC"); //$NON-NLS-1$ + sqlShiftUpIndex = builder.toString() + " */"; //$NON-NLS-1$ + + // ----------- read current index offset -------------- + builder = new StringBuilder(); + builder.append("SELECT MIN("); //$NON-NLS-1$ + builder.append(CDODBSchema.LIST_IDX); + builder.append(") FROM "); //$NON-NLS-1$ + builder.append(getTable()); + builder.append(" WHERE "); //$NON-NLS-1$ + builder.append(CDODBSchema.LIST_REVISION_ID); + builder.append("=?"); //$NON-NLS-1$ + sqlReadCurrentIndexOffset = builder.toString(); + } + + @Override + public void addSimpleChunkWhere(IDBStoreAccessor accessor, CDOID cdoid, StringBuilder builder, int index) + { + int offset = getCurrentIndexOffset(accessor, cdoid); + super.addSimpleChunkWhere(accessor, cdoid, builder, index + offset); + } + + @Override + public void addRangedChunkWhere(IDBStoreAccessor accessor, CDOID cdoid, StringBuilder builder, int fromIndex, + int toIndex) + { + int offset = getCurrentIndexOffset(accessor, cdoid); + super.addRangedChunkWhere(accessor, cdoid, builder, fromIndex + offset, toIndex + offset); } @Override @@ -183,11 +231,12 @@ public class NonAuditListTableMapping extends AbstractListTableMapping implement */ public void clearList(IDBStoreAccessor accessor, CDOID id) { + IPreparedStatementCache statementCache = accessor.getStatementCache(); PreparedStatement stmt = null; try { - stmt = accessor.getStatementCache().getPreparedStatement(sqlClear, ReuseProbability.HIGH); + stmt = statementCache.getPreparedStatement(sqlClear, ReuseProbability.HIGH); getMappingStrategy().getStore().getIDHandler().setCDOID(stmt, 1, id); DBUtil.update(stmt, false); } @@ -197,7 +246,37 @@ public class NonAuditListTableMapping extends AbstractListTableMapping implement } finally { - accessor.getStatementCache().releasePreparedStatement(stmt); + statementCache.releasePreparedStatement(stmt); + } + } + + public int getCurrentIndexOffset(IDBStoreAccessor accessor, CDOID id) + { + IPreparedStatementCache statementCache = accessor.getStatementCache(); + PreparedStatement stmt = null; + ResultSet rset = null; + + try + { + stmt = statementCache.getPreparedStatement(sqlReadCurrentIndexOffset, ReuseProbability.HIGH); + getMappingStrategy().getStore().getIDHandler().setCDOID(stmt, 1, id); + rset = stmt.executeQuery(); + if (!rset.next()) + { + // list is empty. Return the default offset of 0. + return 0; + } + // return the minimum index which is equal to the current offset. + return rset.getInt(1); + } + catch (SQLException e) + { + throw new DBException(e); + } + finally + { + DBUtil.close(rset); + releaseStatement(accessor, stmt); } } @@ -210,32 +289,14 @@ public class NonAuditListTableMapping extends AbstractListTableMapping implement .getRevisionManager().getRevision(id, main, CDORevision.UNCHUNKED, CDORevision.DEPTH_NONE, true); int oldListSize = originalRevision.getList(getFeature()).size(); - // reset the clear-flag - clearFirst = false; - if (TRACER.isEnabled()) { TRACER.format("ListTableMapping.processDelta for revision {0} - previous list size: {1}", originalRevision, //$NON-NLS-1$ oldListSize); } - if (manipulations == null) - { - manipulations = new ArrayList<ManipulationElement>(); - } - else - { - manipulations.clear(); - } - - // create list and initialize with original indexes - for (int i = 0; i < oldListSize; i++) - { - manipulations.add(createOriginalElement(i)); - } - // let the visitor collect the changes - ListDeltaVisitor visitor = new ListDeltaVisitor(); + ListDeltaVisitor visitor = new ListDeltaVisitor(oldListSize); if (TRACER.isEnabled()) { @@ -247,271 +308,10 @@ public class NonAuditListTableMapping extends AbstractListTableMapping implement listDelta.accept(visitor); } - // TODO: here, we could implement further optimizations. - // e.g., if more than 50% of the list's items are removed, - // it's better to clear the list and reinsert all values - // from scratch. + visitor.postProcess(accessor, id); // finally, write results to the database - writeResultToDatabase(accessor, id); - } - - /** - * Write calculated changes to the database - * - * @param accessor - */ - private void writeResultToDatabase(IDBStoreAccessor accessor, CDOID id) - { - IIDHandler idHandler = getMappingStrategy().getStore().getIDHandler(); - PreparedStatement deleteStmt = null; - PreparedStatement moveStmt = null; - PreparedStatement setValueStmt = null; - PreparedStatement insertStmt = null; - - int deleteCounter = 0; - int moveCounter = 0; - int setValueCounter = 0; - int insertCounter = 0; - - int tempIndex = -1; - - if (TRACER.isEnabled()) - { - TRACER.trace("Writing to database:"); //$NON-NLS-1$ - } - - if (clearFirst) - { - if (TRACER.isEnabled()) - { - TRACER.trace(" - clear list"); //$NON-NLS-1$ - } - - clearList(accessor, id); - } - - try - { - for (ManipulationElement element : manipulations) - { - if (element.is(ManipulationConstants.DELETE)) - { - /* - * Step 1: DELETE all elements e which have e.is(REMOVE) by e.sourceIdx - */ - - if (deleteStmt == null) - { - deleteStmt = accessor.getStatementCache().getPreparedStatement(sqlDeleteItem, ReuseProbability.HIGH); - idHandler.setCDOID(deleteStmt, 1, id); - } - - deleteStmt.setInt(2, element.sourceIndex); - deleteStmt.addBatch(); - deleteCounter++; - - if (TRACER.isEnabled()) - { - TRACER.format(" - delete at {0} ", element.sourceIndex); //$NON-NLS-1$ - } - } - - if (element.is(ManipulationConstants.MOVE)) - { - /* - * Step 2: MOVE all elements e (by e.sourceIdx) which have e.is(MOVE) to temporary idx (-1, -2, -3, -4, ...) - * and store temporary idx in e.tempIndex - */ - if (moveStmt == null) - { - moveStmt = accessor.getStatementCache().getPreparedStatement(sqlUpdateIndex, ReuseProbability.HIGH); - idHandler.setCDOID(moveStmt, 2, id); - } - - moveStmt.setInt(3, element.sourceIndex); // from index - moveStmt.setInt(1, --tempIndex); // to index - element.tempIndex = tempIndex; - moveStmt.addBatch(); - moveCounter++; - - if (TRACER.isEnabled()) - { - TRACER.format(" - move {0} -> {1} ", element.sourceIndex, element.tempIndex); //$NON-NLS-1$ - } - } - } - - /* - * Step 3: move all elements which have to be shifted up or down because of add, remove or move of other elements - * to their proper position. This has to be done in two phases to avoid collisions, as the index has to be unique - */ - int size = manipulations.size(); - - /* Step 3a: shift down */ - for (int i = 0; i < size; i++) - { - ManipulationElement element = manipulations.get(i); - - if ((element.type == ManipulationConstants.NONE || element.type == ManipulationConstants.SET_VALUE) - && element.sourceIndex > element.destinationIndex) - { - if (moveStmt == null) - { - moveStmt = accessor.getStatementCache().getPreparedStatement(sqlUpdateIndex, ReuseProbability.HIGH); - idHandler.setCDOID(moveStmt, 2, id); - } - - moveStmt.setInt(3, element.sourceIndex); // from index - moveStmt.setInt(1, element.destinationIndex); // to index - moveStmt.addBatch(); - moveCounter++; - if (TRACER.isEnabled()) - { - TRACER.format(" - move {0} -> {1} ", element.sourceIndex, element.destinationIndex); //$NON-NLS-1$ - } - } - } - - /* Step 3b: shift up */ - for (int i = size - 1; i >= 0; i--) - { - ManipulationElement element = manipulations.get(i); - - if ((element.type == ManipulationConstants.NONE || element.type == ManipulationConstants.SET_VALUE) - && element.sourceIndex < element.destinationIndex) - { - if (moveStmt == null) - { - moveStmt = accessor.getStatementCache().getPreparedStatement(sqlUpdateIndex, ReuseProbability.HIGH); - idHandler.setCDOID(moveStmt, 2, id); - } - - moveStmt.setInt(3, element.sourceIndex); // from index - moveStmt.setInt(1, element.destinationIndex); // to index - moveStmt.addBatch(); - moveCounter++; - if (TRACER.isEnabled()) - { - TRACER.format(" - move {0} -> {1} ", element.sourceIndex, element.destinationIndex); //$NON-NLS-1$ - } - } - } - - for (ManipulationElement element : manipulations) - { - if (element.is(ManipulationConstants.MOVE)) - { - /* - * Step 4: MOVE all elements e have e.is(MOVE) from e.tempIdx to e.destinationIdx (because we have moved them - * before, moveStmt is always initialized - */ - moveStmt.setInt(3, element.tempIndex); // from index - moveStmt.setInt(1, element.destinationIndex); // to index - element.tempIndex = tempIndex; - moveStmt.addBatch(); - moveCounter++; - - if (TRACER.isEnabled()) - { - TRACER.format(" - move {0} -> {1} ", element.tempIndex, element.destinationIndex); //$NON-NLS-1$ - } - } - - if (element.is(ManipulationConstants.SET_VALUE)) - { - /* - * Step 5: SET all elements which have e.type == SET_VALUE by index == e.destinationIdx - */ - if (setValueStmt == null) - { - setValueStmt = accessor.getStatementCache().getPreparedStatement(sqlUpdateValue, ReuseProbability.HIGH); - idHandler.setCDOID(setValueStmt, 2, id); - } - - setValueStmt.setInt(3, element.destinationIndex); - getTypeMapping().setValue(setValueStmt, 1, element.value); - setValueStmt.addBatch(); - setValueCounter++; - - if (TRACER.isEnabled()) - { - TRACER.format(" - set value at {0} to {1} ", element.destinationIndex, element.value); //$NON-NLS-1$ - } - } - - if (element.is(ManipulationConstants.INSERT)) - { - /* - * Step 6: INSERT all elements which have e.type == INSERT. - */ - if (insertStmt == null) - { - insertStmt = accessor.getStatementCache().getPreparedStatement(sqlInsertValue, ReuseProbability.HIGH); - idHandler.setCDOID(insertStmt, 1, id); - } - - insertStmt.setInt(2, element.destinationIndex); - getTypeMapping().setValue(insertStmt, 3, element.value); - insertStmt.addBatch(); - insertCounter++; - - if (TRACER.isEnabled()) - { - TRACER.format(" - insert value at {0} : value {1} ", element.destinationIndex, element.value); //$NON-NLS-1$ - } - } - } - - // finally perform all operations - if (deleteCounter > 0) - { - if (TRACER.isEnabled()) - { - TRACER.format("Performing {0} delete operations", deleteCounter); //$NON-NLS-1$ - } - - DBUtil.executeBatch(deleteStmt, deleteCounter); - } - - if (moveCounter > 0) - { - if (TRACER.isEnabled()) - { - TRACER.format("Performing {0} move operations", moveCounter); //$NON-NLS-1$ - } - - DBUtil.executeBatch(moveStmt, moveCounter); - } - - if (insertCounter > 0) - { - if (TRACER.isEnabled()) - { - TRACER.format("Performing {0} insert operations", insertCounter); //$NON-NLS-1$ - } - - DBUtil.executeBatch(insertStmt, insertCounter); - } - - if (setValueCounter > 0) - { - if (TRACER.isEnabled()) - { - TRACER.format("Performing {0} set operations", setValueCounter); //$NON-NLS-1$ - } - - DBUtil.executeBatch(setValueStmt, setValueCounter); - } - } - catch (SQLException e) - { - throw new DBException(e); - } - finally - { - releaseStatement(accessor, deleteStmt, moveStmt, insertStmt, setValueStmt); - } + visitor.writeResultToDatabase(accessor, id); } private void releaseStatement(IDBStoreAccessor accessor, PreparedStatement... stmts) @@ -558,80 +358,33 @@ public class NonAuditListTableMapping extends AbstractListTableMapping implement } /** - * Helper method: shift all (destination) indexes in the interval [from,to] (inclusive at both ends) by offset - * (positive or negative). + * @author Eike Stepper */ - private void shiftIndexes(int from, int to, int offset) + private final class ListDeltaVisitor implements CDOFeatureDeltaVisitor { - for (ManipulationElement e : manipulations) - { - if (e.destinationIndex >= from && (to == UNBOUNDED_SHIFT || e.destinationIndex <= to)) - { - e.destinationIndex += offset; - } - } - } + private boolean clearFirst; - /** - * Find a manipulation item by destination index). - */ - private ManipulationElement findElement(int index) - { - for (ManipulationElement e : manipulations) - { - if (e.destinationIndex == index) - { - return e; - } - } + private ArrayList<ManipulationElement> manipulations; - // never reached - Assert.isTrue(false); - return null; - } + /** + * Start of a range [tempIndex, tempIndex-1, ...] which lies outside of the normal list indexes and which serve as + * temporary space to move items temporarily to get them out of the way of other operations. + */ + private int tempIndex = -1; - /** - * Delete an element (used in remove and clear) - */ - private void deleteItem(ManipulationElement e) - { - if (e.is(ManipulationConstants.INSERT)) - { - // newly inserted items are simply removed, as - // removing inserted items is equal to no change at all. - manipulations.remove(e); - } - else + public ListDeltaVisitor(int oldListSize) { - // mark the existing item as to be deleted. - // (previous MOVE and SET conditions are overridden by setting - // the exclusive DELETE type). - e.type = ManipulationConstants.DELETE; - e.destinationIndex = ManipulationConstants.NO_INDEX; - } - } - - /** - * Create a ManipulationElement which represents an element which already is in the list. - */ - private ManipulationElement createOriginalElement(int index) - { - return new ManipulationElement(index, index, ManipulationConstants.NIL, ManipulationConstants.NONE); - } + // reset the clear-flag + clearFirst = false; + manipulations = new ArrayList<ManipulationElement>(oldListSize); - /** - * Create a ManipulationElement which represents an element which is inserted in the list. - */ - private ManipulationElement createInsertedElement(int index, Object value) - { - return new ManipulationElement(ManipulationConstants.NONE, index, value, ManipulationConstants.INSERT); - } + // create list and initialize with original indexes + for (int i = 0; i < oldListSize; i++) + { + manipulations.add(ManipulationElement.createOriginalElement(i)); + } + } - /** - * @author Eike Stepper - */ - private final class ListDeltaVisitor implements CDOFeatureDeltaVisitor - { public void visit(CDOAddFeatureDelta delta) { if (TRACER.isEnabled()) @@ -643,7 +396,7 @@ public class NonAuditListTableMapping extends AbstractListTableMapping implement shiftIndexes(delta.getIndex(), UNBOUNDED_SHIFT, +1); // create the item - manipulations.add(createInsertedElement(delta.getIndex(), delta.getValue())); + manipulations.add(ManipulationElement.createInsertedElement(delta.getIndex(), delta.getValue())); } public void visit(CDORemoveFeatureDelta delta) @@ -765,12 +518,571 @@ public class NonAuditListTableMapping extends AbstractListTableMapping implement // never called Assert.isTrue(false); } + + /** + * Helper method: shift all (destination) indexes in the interval [from,to] (inclusive at both ends) by offset + * (positive or negative). + */ + private void shiftIndexes(int from, int to, int offset) + { + for (ManipulationElement e : manipulations) + { + if (e.destinationIndex >= from && (to == UNBOUNDED_SHIFT || e.destinationIndex <= to)) + { + e.destinationIndex += offset; + } + } + } + + /** + * Find a manipulation item by destination index). + */ + private ManipulationElement findElement(int index) + { + for (ManipulationElement e : manipulations) + { + if (e.destinationIndex == index) + { + return e; + } + } + + // never reached + Assert.isTrue(false); + return null; + } + + /** + * Delete an element (used in remove and clear) + */ + private void deleteItem(ManipulationElement e) + { + if (e.is(ManipulationConstants.INSERT)) + { + // newly inserted items are simply removed, as + // removing inserted items is equal to no change at all. + manipulations.remove(e); + } + else + { + // mark the existing item as to be deleted. + // (previous MOVE and SET conditions are overridden by setting + // the exclusive DELETE type). + e.type = ManipulationConstants.DELETE; + e.destinationIndex = ManipulationConstants.NO_INDEX; + } + } + + /** + * Called after all deltas are applied an before the results are written to the database. This method post-processes + * the manipulation elements in order to minimize database access. + */ + public void postProcess(IDBStoreAccessor accessor, CDOID id) + { + if (!((HorizontalNonAuditMappingStrategy)getMappingStrategy()).shallForceZeroBasedIndex()) + { + /* + * this is an optimization which reduces the amount of modifications on the database to maintain list indexes. + * For the optimization, we let go of the assumption that indexes are zero-based. Instead, we work with an + * offset at the database level which can change with every change to the list (e.g. if the second element is + * removed from a list with 1000 elements, instead of shifting down indexes 2 to 1000 by 1, we shift up index 0 + * by 1 and have now a list with indexes starting at 1 instead of 0. This optimization is applied by modifying + * the list of ManipulationElements, which can be seen as the database modification plan. + */ + + // first, get the current offset + int offsetBefore = getCurrentIndexOffset(accessor, id); + if (TRACER.isEnabled()) + { + TRACER.trace("Offset optimization."); //$NON-NLS-1$ + TRACER.trace("Current offset = " + offsetBefore); //$NON-NLS-1$ + } + + applyOffsetToSourceIndexes(offsetBefore); + + int offsetAfter; + + if ((long)Math.abs(offsetBefore) + (long)manipulations.size() > Integer.MAX_VALUE) + { + // security belt for really huge collections or for collections that have been manipulated lots of times + // -> do not optimize after this border is crossed. Instead, reset offset for the whole list to a zero-based + // index. + offsetAfter = 0; + } + else + { + offsetAfter = calculateOptimalOffset(); + } + + if (TRACER.isEnabled()) + { + TRACER.trace("New offset = " + -offsetAfter); //$NON-NLS-1$ + } + + applyOffsetToDestinationIndexes(offsetAfter); + + // make sure temporary indexes do not get in the way of the other operations + tempIndex = Math.min(offsetBefore, offsetAfter) - 1; + } + } + + /** + * Calculate the optimal offset wrt the manipulations planned. The optimal offset is the offset which occurs the + * most in the manipulations (because letting this offset be neutral leads to the least manipulations. Note: the + * zero offset is also regarded as an offset as any other, because selecting an offset != 0 would also lead to + * elements with original offset 0 to be moved. + */ + private int calculateOptimalOffset() + { + HashMap<Integer, Integer> occurrences = new HashMap<Integer, Integer>(); + int bestOffset = 0; + int bestOffsetOccurrence = 0; + + for (ManipulationElement element : manipulations) + { + int srcIdx = element.sourceIndex; + int destIdx = element.destinationIndex; + if (srcIdx != ManipulationConstants.NO_INDEX && destIdx != ManipulationConstants.NO_INDEX) + { + int offset = destIdx - srcIdx; + Integer oldOccurrence = occurrences.get(offset); + int newOccurrence; + if (oldOccurrence == null) + { + newOccurrence = 1; + } + else + { + newOccurrence = oldOccurrence + 1; + } + occurrences.put(offset, newOccurrence); + + // remember maximum along the way + if (newOccurrence > bestOffsetOccurrence) + { + bestOffsetOccurrence = newOccurrence; + bestOffset = offset; + } + } + } + + return bestOffset; + } + + private void applyOffsetToSourceIndexes(int offsetBefore) + { + for (ManipulationElement element : manipulations) + { + if (element.sourceIndex != ManipulationConstants.NO_INDEX) + { + element.sourceIndex += offsetBefore; + } + } + } + + private void applyOffsetToDestinationIndexes(int offsetAfter) + { + for (ManipulationElement element : manipulations) + { + if (element.destinationIndex != ManipulationConstants.NO_INDEX) + { + // substract the offset from all indices to make them relative to the new offset + element.destinationIndex -= offsetAfter; + } + } + } + + /** + * Write calculated changes to the database + * + * @param accessor + */ + private void writeResultToDatabase(IDBStoreAccessor accessor, CDOID id) + { + IIDHandler idHandler = getMappingStrategy().getStore().getIDHandler(); + IPreparedStatementCache statementCache = accessor.getStatementCache(); + PreparedStatement deleteStmt = null; + PreparedStatement moveStmt = null; + PreparedStatement setValueStmt = null; + PreparedStatement insertStmt = null; + + int deleteCounter = 0; + int moveCounter = 0; + int setValueCounter = 0; + int insertCounter = 0; + + if (TRACER.isEnabled()) + { + TRACER.trace("Writing to database:"); //$NON-NLS-1$ + } + + if (clearFirst) + { + if (TRACER.isEnabled()) + { + TRACER.trace(" - clear list"); //$NON-NLS-1$ + } + + clearList(accessor, id); + } + + try + { + for (ManipulationElement element : manipulations) + { + if (element.is(ManipulationConstants.DELETE)) + { + /* + * Step 1: DELETE all elements e which have e.is(REMOVE) by e.sourceIdx + */ + + if (deleteStmt == null) + { + deleteStmt = statementCache.getPreparedStatement(sqlDeleteItem, ReuseProbability.HIGH); + idHandler.setCDOID(deleteStmt, 1, id); + } + + deleteStmt.setInt(2, element.sourceIndex); + deleteStmt.addBatch(); + deleteCounter++; + + if (TRACER.isEnabled()) + { + TRACER.format(" - delete at {0} ", element.sourceIndex); //$NON-NLS-1$ + } + } + + if (element.is(ManipulationConstants.MOVE)) + { + /* + * Step 2: MOVE all elements e (by e.sourceIdx) which have e.is(MOVE) to temporary idx (-1, -2, -3, -4, ...) + * and store temporary idx in e.tempIndex + */ + if (moveStmt == null) + { + moveStmt = statementCache.getPreparedStatement(sqlUpdateIndex, ReuseProbability.HIGH); + idHandler.setCDOID(moveStmt, 2, id); + } + + moveStmt.setInt(3, element.sourceIndex); // from index + moveStmt.setInt(1, --tempIndex); // to index + element.tempIndex = tempIndex; + moveStmt.addBatch(); + moveCounter++; + + if (TRACER.isEnabled()) + { + TRACER.format(" - move {0} -> {1} ", element.sourceIndex, element.tempIndex); //$NON-NLS-1$ + } + } + } + + /* now perform deletes and moves ... */ + if (deleteCounter > 0) + { + if (TRACER.isEnabled()) + { + TRACER.format("Performing {0} delete operations", deleteCounter); //$NON-NLS-1$ + } + + DBUtil.executeBatch(deleteStmt, deleteCounter); + } + + if (moveCounter > 0) + { + if (TRACER.isEnabled()) + { + TRACER.format("Performing {0} move operations", moveCounter); //$NON-NLS-1$ + } + + DBUtil.executeBatch(moveStmt, moveCounter); + moveStmt.clearBatch(); + moveCounter = 0; + } + + writeShiftOperations(accessor, id); + + for (ManipulationElement element : manipulations) + { + if (element.is(ManipulationConstants.MOVE)) + { + /* + * Step 4: MOVE all elements e have e.is(MOVE) from e.tempIdx to e.destinationIdx (because we have moved + * them before, moveStmt is always initialized + */ + moveStmt.setInt(3, element.tempIndex); // from index + moveStmt.setInt(1, element.destinationIndex); // to index + moveStmt.addBatch(); + moveCounter++; + + if (TRACER.isEnabled()) + { + TRACER.format(" - move {0} -> {1} ", element.tempIndex, element.destinationIndex); //$NON-NLS-1$ + } + } + + if (element.is(ManipulationConstants.SET_VALUE)) + { + /* + * Step 5: SET all elements which have e.type == SET_VALUE by index == e.destinationIdx + */ + if (setValueStmt == null) + { + setValueStmt = statementCache.getPreparedStatement(sqlUpdateValue, ReuseProbability.HIGH); + idHandler.setCDOID(setValueStmt, 2, id); + } + + setValueStmt.setInt(3, element.destinationIndex); + getTypeMapping().setValue(setValueStmt, 1, element.value); + setValueStmt.addBatch(); + setValueCounter++; + + if (TRACER.isEnabled()) + { + TRACER.format(" - set value at {0} to {1} ", element.destinationIndex, element.value); //$NON-NLS-1$ + } + } + + if (element.is(ManipulationConstants.INSERT)) + { + /* + * Step 6: INSERT all elements which have e.type == INSERT. + */ + if (insertStmt == null) + { + insertStmt = statementCache.getPreparedStatement(sqlInsertValue, ReuseProbability.HIGH); + idHandler.setCDOID(insertStmt, 1, id); + } + + insertStmt.setInt(2, element.destinationIndex); + getTypeMapping().setValue(insertStmt, 3, element.value); + insertStmt.addBatch(); + insertCounter++; + + if (TRACER.isEnabled()) + { + TRACER.format(" - insert value at {0} : value {1} ", element.destinationIndex, element.value); //$NON-NLS-1$ + } + } + } + + if (moveCounter > 0) + { + if (TRACER.isEnabled()) + { + TRACER.format("Performing {0} move operations", moveCounter); //$NON-NLS-1$ + } + + DBUtil.executeBatch(moveStmt, moveCounter); + } + + if (insertCounter > 0) + { + if (TRACER.isEnabled()) + { + TRACER.format("Performing {0} insert operations", insertCounter); //$NON-NLS-1$ + } + + DBUtil.executeBatch(insertStmt, insertCounter); + } + + if (setValueCounter > 0) + { + if (TRACER.isEnabled()) + { + TRACER.format("Performing {0} set operations", setValueCounter); //$NON-NLS-1$ + } + + DBUtil.executeBatch(setValueStmt, setValueCounter); + } + } + catch (SQLException e) + { + throw new DBException(e); + } + finally + { + releaseStatement(accessor, deleteStmt, moveStmt, insertStmt, setValueStmt); + } + } + + /** + * Perform the shift operations to adjust indexes resulting from remove, insert, and move operations. + * + * @see #writeResultToDatabase(IDBStoreAccessor, CDOID) + * @throws SQLException + */ + private void writeShiftOperations(IDBStoreAccessor accessor, CDOID id) throws SQLException + { + /* + * Step 3: shift all elements which have to be shifted up or down because of add, remove or move of other elements + * to their proper position. This has to be done in two phases to avoid collisions, as the index has to be unique + * and shift up operations have to be executed in top to bottom order. + */ + + IIDHandler idHandler = getMappingStrategy().getStore().getIDHandler(); + int size = manipulations.size(); + + LinkedList<ShiftOperation> shiftOperations = new LinkedList<ShiftOperation>(); + + /* + * If a necessary shift is detected (source and destination indices differ), firstIndex is set to the current + * index and currentOffset is set to the offset of the shift operation. When a new offset is detected or the range + * is interrupted, we record the range and start a new one if needed. + */ + int rangeStartIndex = ManipulationConstants.NO_INDEX; + int rangeOffset = 0; + int lastElementIndex = ManipulationConstants.NO_INDEX; + + // iterate through the manipulationElements and collect the necessary operations + for (int i = 0; i < size; i++) + { + ManipulationElement element = manipulations.get(i); + + /* + * shift applies only to elements which are not moved, inserted or deleted (i.e. only plain SET_VALUE and NONE + * are affected) + */ + if (element.type == ManipulationConstants.NONE || element.type == ManipulationConstants.SET_VALUE) + { + int elementOffset = element.destinationIndex - element.sourceIndex; + + /* + * first make sure if we have to close a previous range. This is the case, if the current element's offset + * differs from the rangeOffset and a range is open. + */ + if (elementOffset != rangeOffset && rangeStartIndex != ManipulationConstants.NO_INDEX) + { + // there is an open range but the rangeOffset differs. We have to close the open range + shiftOperations.add(new ShiftOperation(rangeStartIndex, lastElementIndex, rangeOffset)); + // and reset the state + rangeStartIndex = ManipulationConstants.NO_INDEX; + rangeOffset = 0; + } + + /* + * at this point, either a range is open, which means that the current element also fits in the range (i.e. + * the offsets match) or no range is open. In the latter case, we have to open one if the current element's + * offset is not 0. + */ + if (elementOffset != 0 && rangeStartIndex == ManipulationConstants.NO_INDEX) + { + rangeStartIndex = element.sourceIndex; + rangeOffset = elementOffset; + } + } + else + { // shift does not apply to this element because of its type + if (rangeStartIndex != ManipulationConstants.NO_INDEX) + { + // if there is an open range, we have to close and remember it + shiftOperations.add(new ShiftOperation(rangeStartIndex, lastElementIndex, rangeOffset)); + // and reset the state + rangeStartIndex = ManipulationConstants.NO_INDEX; + rangeOffset = 0; + } + } + lastElementIndex = element.sourceIndex; + } + + // after the iteration, we have to make sure that we remember the last open range, if it is there + if (rangeStartIndex != ManipulationConstants.NO_INDEX) + { + shiftOperations.add(new ShiftOperation(rangeStartIndex, lastElementIndex, rangeOffset)); + } + + /* + * now process the operations. Move down operations can be performed directly, move up operations need to be + * performed later in the reverse direction + */ + ListIterator<ShiftOperation> operationIt = shiftOperations.listIterator(); + + IPreparedStatementCache statementCache = accessor.getStatementCache(); + PreparedStatement shiftDownStmt = null; + int operationCounter = 0; + + try + { + + while (operationIt.hasNext()) + { + ShiftOperation operation = operationIt.next(); + if (operation.offset < 0) + { + if (shiftDownStmt == null) + { + shiftDownStmt = statementCache.getPreparedStatement(sqlShiftDownIndex, ReuseProbability.HIGH); + idHandler.setCDOID(shiftDownStmt, 2, id); + } + + if (TRACER.isEnabled()) + { + TRACER.format(" - shift down {0} ", operation); //$NON-NLS-1$ + } + + shiftDownStmt.setInt(1, operation.offset); + shiftDownStmt.setInt(3, operation.startIndex); + shiftDownStmt.setInt(4, operation.endIndex); + shiftDownStmt.addBatch(); + operationCounter++; + + operationIt.remove(); + } + } + if (operationCounter > 0) + { + DBUtil.executeBatch(shiftDownStmt, operationCounter, false); + } + } + finally + { + releaseStatement(accessor, shiftDownStmt); + } + + PreparedStatement shiftUpStmt = null; + operationCounter = 0; + + try + { + + while (operationIt.hasPrevious()) + { + ShiftOperation operation = operationIt.previous(); + if (shiftUpStmt == null) + { + shiftUpStmt = statementCache.getPreparedStatement(sqlShiftUpIndex, ReuseProbability.HIGH); + idHandler.setCDOID(shiftUpStmt, 2, id); + } + + if (TRACER.isEnabled()) + { + TRACER.format(" - shift up {0} ", operation); //$NON-NLS-1$ + } + + shiftUpStmt.setInt(1, operation.offset); + shiftUpStmt.setInt(3, operation.startIndex); + shiftUpStmt.setInt(4, operation.endIndex); + shiftUpStmt.addBatch(); + operationCounter++; + } + + if (operationCounter > 0) + { + DBUtil.executeBatch(shiftUpStmt, operationCounter, false); + } + } + finally + { + releaseStatement(accessor, shiftUpStmt); + } + } + } /** * @author Eike Stepper */ - private static final class ManipulationConstants + private static interface ManipulationConstants { public static final int NO_INDEX = Integer.MIN_VALUE; @@ -790,7 +1102,7 @@ public class NonAuditListTableMapping extends AbstractListTableMapping implement /** * @author Eike Stepper */ - private static final class ManipulationElement + private static final class ManipulationElement implements ManipulationConstants { public int type; @@ -805,12 +1117,28 @@ public class NonAuditListTableMapping extends AbstractListTableMapping implement public ManipulationElement(int srcIdx, int dstIdx, Object val, int t) { sourceIndex = srcIdx; - tempIndex = ManipulationConstants.NONE; + tempIndex = NO_INDEX; destinationIndex = dstIdx; value = val; type = t; } + /** + * Create a ManipulationElement which represents an element which already is in the list. + */ + public static ManipulationElement createOriginalElement(int index) + { + return new ManipulationElement(index, index, NIL, NONE); + } + + /** + * Create a ManipulationElement which represents an element which is inserted in the list. + */ + public static ManipulationElement createInsertedElement(int index, Object value) + { + return new ManipulationElement(NO_INDEX, index, value, ManipulationConstants.INSERT); + } + public boolean is(int t) { return (type & t) > 0; @@ -821,4 +1149,29 @@ public class NonAuditListTableMapping extends AbstractListTableMapping implement type |= t; } } + + /** + * @author Eike Stepper + */ + private static class ShiftOperation + { + final int startIndex; + + final int endIndex; + + final int offset; + + ShiftOperation(int startIndex, int endIndex, int offset) + { + this.startIndex = startIndex; + this.endIndex = endIndex; + this.offset = offset; + } + + @Override + public String toString() + { + return "range [" + startIndex + ".." + endIndex + "] offset " + offset; + } + } } diff --git a/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/RevisionDeltaTest.java b/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/RevisionDeltaTest.java index 3659cee704..f48e803fb0 100644 --- a/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/RevisionDeltaTest.java +++ b/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/RevisionDeltaTest.java @@ -531,6 +531,138 @@ public class RevisionDeltaTest extends AbstractCDOTest }); } + public void testMultipleInserts() + { + testStoreDelta(new ListManipulator() + { + @SuppressWarnings("unchecked") + public void doManipulations(EList<?> list) + { + EList<Company> l = (EList<Company>)list; + Company company = getModel1Factory().createCompany(); + company.setName("NewEntry 1"); + l.add(7, company); + company = getModel1Factory().createCompany(); + company.setName("NewEntry 2"); + l.add(12, company); + } + }); + } + + public void testInsertAndRemove() + { + testStoreDelta(new ListManipulator() + { + @SuppressWarnings("unchecked") + public void doManipulations(EList<?> list) + { + EList<Company> l = (EList<Company>)list; + Company company = getModel1Factory().createCompany(); + company.setName("NewEntry 1"); + l.add(7, company); + l.remove(12); + } + }); + } + + public void testInsertAndMove() + { + testStoreDelta(new ListManipulator() + { + @SuppressWarnings("unchecked") + public void doManipulations(EList<?> list) + { + EList<Company> l = (EList<Company>)list; + Company company = getModel1Factory().createCompany(); + company.setName("NewEntry 1"); + l.add(7, company); + l.move(12, 7); + } + }); + } + + public void testMoveAndDelete() + { + testStoreDelta(new ListManipulator() + { + @SuppressWarnings("unchecked") + public void doManipulations(EList<?> list) + { + EList<Company> l = (EList<Company>)list; + l.move(12, 7); + l.remove(12); + } + }); + } + + public void testInsertAndMoveAndRemove() + { + testStoreDelta(new ListManipulator() + { + @SuppressWarnings("unchecked") + public void doManipulations(EList<?> list) + { + EList<Company> l = (EList<Company>)list; + Company company = getModel1Factory().createCompany(); + company.setName("NewEntry 1"); + l.add(7, company); + l.move(12, 7); + l.remove(12); + } + }); + } + + public void testInsertAndSet() + { + testStoreDelta(new ListManipulator() + { + @SuppressWarnings("unchecked") + public void doManipulations(EList<?> list) + { + EList<Company> l = (EList<Company>)list; + Company company = getModel1Factory().createCompany(); + company.setName("NewEntry 1"); + l.add(7, company); + Company company2 = getModel1Factory().createCompany(); + company2.setName("NewEntry 2"); + l.set(7, company2); + l.add(19, company); // <- needed because the set operation makes the company a dangling reference + } + }); + } + + public void testSetAndRemove() + { + testStoreDelta(new ListManipulator() + { + @SuppressWarnings("unchecked") + public void doManipulations(EList<?> list) + { + EList<Company> l = (EList<Company>)list; + Company company = getModel1Factory().createCompany(); + company.setName("NewEntry 1"); + Company oldCompany = l.get(7); + l.set(7, company); + l.add(19, oldCompany); + l.remove(7); // <- needed because the set operation makes the company a dangling reference + } + }); + } + + public void testMultipleRemoves() + { + testStoreDelta(new ListManipulator() + { + @SuppressWarnings("unchecked") + public void doManipulations(EList<?> list) + { + EList<Company> l = (EList<Company>)list; + l.remove(7); + l.remove(12); + } + }); + } + private InternalCDORevision getCopyCDORevision(Object object) { return (InternalCDORevision)CDOUtil.getCDOObject((EObject)object).cdoRevision().copy(); diff --git a/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/performance/DeletePerformanceTest.java b/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/performance/DeletePerformanceTest.java index 4e053e412a..c1ae50d895 100644 --- a/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/performance/DeletePerformanceTest.java +++ b/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/performance/DeletePerformanceTest.java @@ -55,17 +55,17 @@ public class DeletePerformanceTest extends PerformanceTest } @CleanRepositoriesBefore - public void testDeleteRandom() throws Exception + public void _testDeleteRandom() throws Exception { Company company = initModel(); CDOTransaction transaction = (CDOTransaction)CDOUtil.getCDOObject(company).cdoView(); msg("Starting to remove elements ..."); - for (int i = 0; i < AMOUNT_ELEMENTS / 2; i++) + for (int i = 0; i < AMOUNT_ELEMENTS / 4; i++) { int currentSize = AMOUNT_ELEMENTS - i; - int indexToRemove = random.nextInt(currentSize); + int indexToRemove = random.nextInt(currentSize - 1); company.getCategories().remove(indexToRemove); @@ -76,14 +76,14 @@ public class DeletePerformanceTest extends PerformanceTest } @CleanRepositoriesBefore - public void testDeleteDeterministic() throws Exception + public void testDeleteEveryOther() throws Exception { Company company = initModel(); CDOTransaction transaction = (CDOTransaction)CDOUtil.getCDOObject(company).cdoView(); msg("Starting to remove elements ..."); - for (int i = 0; i < AMOUNT_ELEMENTS / 2; i++) + for (int i = 0; i < AMOUNT_ELEMENTS / 4; i++) { int indexToRemove = AMOUNT_ELEMENTS / 2 - i; @@ -95,4 +95,23 @@ public class DeletePerformanceTest extends PerformanceTest } } + @CleanRepositoriesBefore + public void _testDeleteAtBeginning() throws Exception + { + Company company = initModel(); + CDOTransaction transaction = (CDOTransaction)CDOUtil.getCDOObject(company).cdoView(); + + msg("Starting to remove elements ..."); + + for (int i = 0; i < AMOUNT_ELEMENTS / 4; i++) + { + int indexToRemove = 0; + + company.getCategories().remove(indexToRemove); + + startProbing(); + transaction.commit(); + stopProbing(); + } + } } diff --git a/plugins/org.eclipse.net4j.db/src/org/eclipse/net4j/db/DBUtil.java b/plugins/org.eclipse.net4j.db/src/org/eclipse/net4j/db/DBUtil.java index 0aa500536f..ac34132b4b 100644 --- a/plugins/org.eclipse.net4j.db/src/org/eclipse/net4j/db/DBUtil.java +++ b/plugins/org.eclipse.net4j.db/src/org/eclipse/net4j/db/DBUtil.java @@ -523,6 +523,14 @@ public final class DBUtil */ public static void executeBatch(PreparedStatement stmt, int counter) { + executeBatch(stmt, counter, true); + } + + /** + * @since 4.1 + */ + public static void executeBatch(PreparedStatement stmt, int counter, boolean checkExactlyOne) + { try { int[] results = stmt.executeBatch(); @@ -534,10 +542,14 @@ public final class DBUtil for (int i = 0; i < results.length; i++) { int result = results[i]; - if (result != 1 && result != Statement.SUCCESS_NO_INFO) + if (result < 0 && result != Statement.SUCCESS_NO_INFO) { throw new DBException("Result " + i + " is not successful: " + result); } + else if (checkExactlyOne && result != 1) + { + throw new DBException("Result " + i + " did not affect exactly one row: " + result); + } } } catch (SQLException ex) |