Index: extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/LargeValueEntity.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/LargeValueEntity.java (revision 1705) +++ extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/LargeValueEntity.java (working copy) @@ -25,6 +25,7 @@ package org.modeshape.connector.store.jpa.model.simple; import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; +import java.util.List; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EntityManager; @@ -32,6 +33,7 @@ import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.Id; import javax.persistence.Lob; +import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.Query; import javax.persistence.Table; @@ -47,7 +49,10 @@ import org.modeshape.graph.property.PropertyType; */ @Entity @Table( name = "MODE_SIMPLE_LARGE_VALUES" ) -@NamedQuery( name = "LargeValueEntity.deleteUnused", query = "delete LargeValueEntity value where value.hash not in (select values.hash from NodeEntity node join node.largeValues values)" ) +@NamedQueries( { + @NamedQuery( name = "LargeValueEntity.selectUnused", query = "select values.hash from NodeEntity node join node.largeValues values" ), + @NamedQuery( name = "LargeValueEntity.deleteAllUnused", query = "delete LargeValueEntity value where value.hash not in (select values.hash from NodeEntity node join node.largeValues values)" ), + @NamedQuery( name = "LargeValueEntity.deleteIn", query = "delete LargeValueEntity value where value.hash in (:inValues)" )} ) public class LargeValueEntity { @Id @@ -184,12 +189,42 @@ public class LargeValueEntity { * Delete all unused large value entities. * * @param manager the manager; never null + * @param dialect the dialect * @return the number of deleted large values */ - public static int deleteUnused( EntityManager manager ) { + @SuppressWarnings( "unchecked" ) + public static int deleteUnused( EntityManager manager, + String dialect ) { assert manager != null; - Query delete = manager.createNamedQuery("LargeValueEntity.deleteUnused"); - int result = delete.executeUpdate(); + + int result = 0; + if (dialect.toLowerCase().indexOf("mysql") != -1) { + // Unfortunately, we cannot delete all the unused large values in a single statement + // because of MySQL (see MODE-691). Therefore, we need to do this in multiple steps: + // 1) Find the set of hashes that are not used anymore + // 2) Delete each of these rows, using bulk deletes with a small number (20) of hashes at a time + + Query select = manager.createNamedQuery("LargeValueEntity.selectUnused"); + List hashes = select.getResultList(); + if (hashes.isEmpty()) return 0; + + // Delete the unused large entities, (up to) 20 at a time + int endIndex = hashes.size(); + int fromIndex = 0; + do { + int toIndex = Math.min(fromIndex + 20, endIndex); + Query query = manager.createQuery("LargeValueEntity.deleteIn"); + query.setParameter("inValues", hashes.subList(fromIndex, toIndex)); + query.executeUpdate(); + result += toIndex - fromIndex; + fromIndex = toIndex; + } while (fromIndex < endIndex); + } else { + // For all dialects other than MySQL, we can just use the one delete statement ... + Query delete = manager.createNamedQuery("LargeValueEntity.deleteAllUnused"); + result = delete.executeUpdate(); + } + manager.flush(); return result; } Index: extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleJpaConnection.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleJpaConnection.java (revision 1705) +++ extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleJpaConnection.java (working copy) @@ -79,7 +79,8 @@ public class SimpleJpaConnection implements RepositoryConnection { this.repository = new SimpleJpaRepository(source.getName(), source.getRootUuid(), source.getDefaultWorkspaceName(), source.getPredefinedWorkspaceNames(), entityManager, source.getRepositoryContext().getExecutionContext(), source.isCompressData(), - source.isCreatingWorkspacesAllowed(), source.getLargeValueSizeInBytes()); + source.isCreatingWorkspacesAllowed(), source.getLargeValueSizeInBytes(), + source.getDialect()); } Index: extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleJpaRepository.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleJpaRepository.java (revision 1705) +++ extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleJpaRepository.java (working copy) @@ -57,9 +57,9 @@ import org.modeshape.connector.store.jpa.util.Namespaces; import org.modeshape.connector.store.jpa.util.Serializer; import org.modeshape.connector.store.jpa.util.Workspaces; import org.modeshape.connector.store.jpa.util.Serializer.LargeValues; -import org.modeshape.graph.ModeShapeLexicon; import org.modeshape.graph.ExecutionContext; import org.modeshape.graph.Location; +import org.modeshape.graph.ModeShapeLexicon; import org.modeshape.graph.connector.LockFailedException; import org.modeshape.graph.connector.map.AbstractMapWorkspace; import org.modeshape.graph.connector.map.MapNode; @@ -106,6 +106,7 @@ public class SimpleJpaRepository extends MapRepository { protected final boolean compressData; protected final boolean creatingWorkspacesAllowed; protected final long minimumSizeOfLargeValuesInBytes; + protected final String dialect; public SimpleJpaRepository( String sourceName, UUID rootNodeUuid, @@ -115,7 +116,8 @@ public class SimpleJpaRepository extends MapRepository { ExecutionContext context, boolean compressData, boolean creatingWorkspacesAllowed, - long minimumSizeOfLargeValuesInBytes ) { + long minimumSizeOfLargeValuesInBytes, + String dialect ) { super(sourceName, rootNodeUuid, defaultWorkspaceName); this.context = context; @@ -126,6 +128,7 @@ public class SimpleJpaRepository extends MapRepository { this.compressData = compressData; this.creatingWorkspacesAllowed = creatingWorkspacesAllowed; this.minimumSizeOfLargeValuesInBytes = minimumSizeOfLargeValuesInBytes; + this.dialect = dialect; this.entityManager = entityManager; workspaceEntities = new Workspaces(entityManager); @@ -139,7 +142,8 @@ public class SimpleJpaRepository extends MapRepository { ExecutionContext context, boolean compressData, boolean creatingWorkspacesAllowed, - long minimumSizeOfLargeValuesInBytes ) { + long minimumSizeOfLargeValuesInBytes, + String dialect ) { super(sourceName, rootNodeUuid); this.context = context; @@ -150,6 +154,7 @@ public class SimpleJpaRepository extends MapRepository { this.compressData = compressData; this.creatingWorkspacesAllowed = creatingWorkspacesAllowed; this.minimumSizeOfLargeValuesInBytes = minimumSizeOfLargeValuesInBytes; + this.dialect = dialect; this.entityManager = entityManager; workspaceEntities = new Workspaces(entityManager); @@ -399,8 +404,12 @@ public class SimpleJpaRepository extends MapRepository { @Override protected void removeUuidReference( MapNode node ) { SubgraphQuery branch = SubgraphQuery.create(entityManager, workspaceId, node.getUuid(), 0); + // Delete in bulk except when using MySql ... branch.deleteSubgraph(true); branch.close(); + + // Delete unused large values ... + LargeValueEntity.deleteUnused(entityManager, dialect); } /* @@ -420,6 +429,9 @@ public class SimpleJpaRepository extends MapRepository { Query query = entityManager.createQuery("NodeEntity.deleteAllInWorkspace"); query.setParameter("workspaceId", workspaceId); query.executeUpdate(); + + // Delete unused large values ... + LargeValueEntity.deleteUnused(entityManager, dialect); } /* Index: extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SubgraphQuery.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SubgraphQuery.java (revision 1705) +++ extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SubgraphQuery.java (working copy) @@ -162,7 +162,7 @@ public class SubgraphQuery { */ @SuppressWarnings( "unchecked" ) public List getNodes( boolean includeRoot, - boolean includeChildrenOfMaxDepthNodes ) { + boolean includeChildrenOfMaxDepthNodes ) { if (query == null) throw new IllegalStateException(); // Now query for all the nodes and put into a list ... Query search = manager.createNamedQuery("SubgraphNodeEntity.getChildEntities"); @@ -216,9 +216,6 @@ public class SubgraphQuery { delete.setParameter("workspaceId", workspaceId); delete.executeUpdate(); - // Delete unused large values ... - LargeValueEntity.deleteUnused(manager); - manager.flush(); } Index: extensions/modeshape-connector-store-jpa/src/test/resources/database.properties =================================================================== --- extensions/modeshape-connector-store-jpa/src/test/resources/database.properties (revision 1705) +++ extensions/modeshape-connector-store-jpa/src/test/resources/database.properties (working copy) @@ -44,7 +44,6 @@ jpaSource.defaultWorkspaceName = ${jpaSource.defaultWorkspaceName} # comma-separated and optionally quoted jpaSource.predefinedWorkspaceNames = ${jpaSource.predefinedWorkspaceNames} jpaSource.model = ${jpaSource.model} -jpaSource.numberOfConnectionsToAcquireAsNeeded = ${jpaSource.numberOfConnectionsToAcquireAsNeeded} jpaSource.referentialIntegrityEnforced = ${jpaSource.referentialIntegrityEnforced} jpaSource.retryLimit = ${jpaSource.retryLimit} jpaSource.rootNodeUuid = ${jpaSource.rootNodeUuid} Index: modeshape-integration-tests/src/test/java/org/modeshape/test/integration/jpa/JcrRepositoryWithJpaSourceTest.java new file mode 100644 =================================================================== --- /dev/null (revision 1705) +++ modeshape-integration-tests/src/test/java/org/modeshape/test/integration/jpa/JcrRepositoryWithJpaSourceTest.java (working copy) @@ -0,0 +1,124 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.modeshape.test.integration.jpa; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import java.net.URL; +import javax.jcr.Credentials; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.modeshape.graph.SecurityContext; +import org.modeshape.jcr.JcrConfiguration; +import org.modeshape.jcr.JcrEngine; +import org.modeshape.jcr.SecurityContextCredentials; +import org.modeshape.test.integration.jpa.JcrRepositoryWithJpaConfigurationTest.CustomSecurityContext; + +public class JcrRepositoryWithJpaSourceTest { + + private JcrEngine engine; + private Repository repository; + private Session session; + private Credentials credentials; + + @Before + public void beforeEach() throws Exception { + final URL configUrl = getClass().getResource("/tck/simple-jpa/configRepository.xml"); + final String workspaceName = "otherWorkspace"; + assert configUrl != null; + + // Load the configuration from the config file ... + JcrConfiguration config = new JcrConfiguration(); + config.loadFrom(configUrl); + + // Start the engine ... + engine = config.build(); + engine.start(); + + // Set up the fake credentials ... + SecurityContext securityContext = new CustomSecurityContext("bill"); + credentials = new SecurityContextCredentials(securityContext); + + repository = engine.getRepository("Test Repository Source"); + assert repository != null; + session = repository.login(credentials, workspaceName); + assert session != null; + } + + @After + public void afterEach() throws Exception { + if (engine != null) { + try { + if (session != null) { + try { + session.logout(); + } finally { + session = null; + } + } + } finally { + repository = null; + try { + engine.shutdown(); + } finally { + engine = null; + } + } + } + } + + // @Test + // public void shouldHaveSession() { + // assertThat(session, is(notNullValue())); + // } + + @Test + public void shouldBeAbleToRemoveNodeThatExists_Mode691() throws RepositoryException { + // Create some content ... + Node root = session.getRootNode(); + Node a = root.addNode("a"); + Node b = a.addNode("b"); + Node c = b.addNode("c"); + Node d1 = c.addNode("d_one"); + Node d2 = c.addNode("d_two"); + session.save(); + + root = session.getRootNode(); + String pathToNode = "a/b"; + assertThat(root.hasNode(pathToNode), is(true)); + + Node nodeToDelete = root.getNode(pathToNode); + nodeToDelete.remove(); + session.save(); + + root = session.getRootNode(); + assertThat(root.hasNode(pathToNode), is(false)); + } + +} Index: pom.xml =================================================================== --- pom.xml (revision 1705) +++ pom.xml (working copy) @@ -170,11 +170,11 @@ - 1 - 0 + 2 + 1 1 100 - 0 + 10 true 150 create @@ -184,7 +184,6 @@ - true 3 @@ -509,7 +508,7 @@ mysql mysql-connector-java - 5.0.5 + 5.1.7 test @@ -536,7 +535,7 @@ postgresql postgresql - 8.4-701.jdbc3 + 8.3-604.jdbc3 test