/* * Copyright 2015 Samppa Saarela * * Licensed 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.javersion.store.jdbc; import static org.javersion.store.jdbc.VersionStatus.ACTIVE; import static org.javersion.store.jdbc.VersionStatus.REDUNDANT; import static org.javersion.store.jdbc.VersionStatus.SQUASHED; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Predicate; import org.javersion.core.OptimizedGraph; import org.javersion.core.Persistent; import org.javersion.core.Revision; import org.javersion.core.VersionNode; import org.javersion.object.ObjectVersionGraph; import org.javersion.path.PropertyPath; import com.querydsl.core.dml.StoreClause; import com.querydsl.core.group.Group; import com.querydsl.core.types.Path; import com.querydsl.sql.dml.SQLInsertClause; public abstract class AbstractUpdateBatch<Id, M, V extends JVersion<Id>, Options extends StoreOptions<Id, M, V>, This extends AbstractUpdateBatch<Id, M, V, Options, This>> implements UpdateBatch<Id, M> { protected static boolean isNotEmpty(StoreClause<?> store) { return store != null && !store.isEmpty(); } protected final Options options; protected final AbstractVersionStoreJdbc<Id, M, V, This, Options> store; protected final SQLInsertClause versionBatch; protected final SQLInsertClause parentBatch; protected final SQLInsertClause propertyBatch; public AbstractUpdateBatch(AbstractVersionStoreJdbc<Id, M, V, This, Options> store) { this.store = store; this.options = store.options; versionBatch = options.queryFactory.insert(options.version); parentBatch = options.queryFactory.insert(options.parent); propertyBatch = options.queryFactory.insert(options.property); } @Override public This addVersion(Id docId, VersionNode<PropertyPath, Object, M> version) { insertVersion(docId, version); insertParents(version); insertProperties(version); return self(); } @Override public void execute() { if (isNotEmpty(versionBatch)) { versionBatch.execute(); if (options.publisher != null) { options.transactions.afterCommit(this::publishAfterCommit); } } if (isNotEmpty(parentBatch)) { parentBatch.execute(); } if (isNotEmpty(propertyBatch)) { propertyBatch.execute(); } } protected This prune(ObjectVersionGraph<M> graph, Predicate<VersionNode<PropertyPath, Object, M>> keep) { OptimizedGraph<PropertyPath, Object, M, ObjectVersionGraph<M>> optimizedGraph = optimizedGraph(graph, keep); if (optimizedGraph != null) { List<Revision> keptRevisions = optimizedGraph.getKeptRevisions(); List<Revision> squashedRevisions = optimizedGraph.getSquashedRevisions(); List<Revision> modifiedRevisions = concat(keptRevisions, squashedRevisions); if (!squashedRevisions.isEmpty()) { deleteParents(modifiedRevisions); deleteProperties(modifiedRevisions); deleteVersions(squashedRevisions); insertOptimizedParentsAndProperties(optimizedGraph.getGraph()); } } return self(); } protected This optimize(ObjectVersionGraph<M> graph, Predicate<VersionNode<PropertyPath, Object, M>> keep) { OptimizedGraph<PropertyPath, Object, M, ObjectVersionGraph<M>> optimizedGraph = optimizedGraph(graph, keep); if (optimizedGraph != null) { List<Revision> squashedRevisions = optimizedGraph.getSquashedRevisions(); if (!squashedRevisions.isEmpty()) { squashVersions(squashedRevisions); deleteRedundantParents(squashedRevisions); deleteRedundantProperties(squashedRevisions); optimizeParentsAndProperties(graph, optimizedGraph.getGraph()); } } return self(); } @SuppressWarnings("unchecked") protected This self() { return (This) this; } protected void publishAfterCommit() { options.publisher.execute(store::publish); } private void optimizeParentsAndProperties(ObjectVersionGraph<M> oldGraph, ObjectVersionGraph<M> newGraph) { newGraph.getVersionNodes().forEach(newVersionNode -> { VersionNode<PropertyPath, Object, M> oldVersionNode = oldGraph.getVersionNode(newVersionNode.revision); optimizeParents(newVersionNode.revision, oldVersionNode.getParentRevisions(), newVersionNode.getParentRevisions()); optimizeProperties(newVersionNode.revision, oldVersionNode.getChangeset(), newVersionNode.getChangeset()); }); } private void optimizeProperties(Revision revision, Map<PropertyPath, Object> oldChangeset, Map<PropertyPath, Object> newChangeset) { newChangeset.forEach((path, value) -> { if (!oldChangeset.containsKey(path)) { insertProperty(revision, path, value, REDUNDANT); } }); oldChangeset.forEach((path, value) -> { if (!newChangeset.containsKey(path)) { squashProperty(revision, path); } }); } private void optimizeParents(Revision revision, Set<Revision> oldParentRevisions, Set<Revision> newParentRevisions) { newParentRevisions.forEach(newParentRevision -> { if (!oldParentRevisions.contains(newParentRevision)) { insertParent(revision, newParentRevision, REDUNDANT); } }); oldParentRevisions.forEach(oldParentRevision -> { if (!newParentRevisions.contains(oldParentRevision)) { squashParent(revision, oldParentRevision); } }); } private <T> List<T> concat(List<T> a, List<T> b) { List<T> combined = new ArrayList<>(a.size() + b.size()); combined.addAll(a); combined.addAll(b); return combined; } private OptimizedGraph<PropertyPath, Object, M, ObjectVersionGraph<M>> optimizedGraph(ObjectVersionGraph<M> graph, Predicate<VersionNode<PropertyPath, Object, M>> keep) { OptimizedGraph<PropertyPath, Object, M, ObjectVersionGraph<M>> optimizedGraph = graph.optimize(keep); if (optimizedGraph.getSquashedRevisions().isEmpty()) { return null; } if (optimizedGraph.getKeptRevisions().isEmpty()) { throw new IllegalArgumentException("keep-predicate didn't match any version"); } return optimizedGraph; } protected void insertVersion(Id docId, VersionNode<PropertyPath, Object, M> version) { versionBatch .set(options.version.docId, docId) .set(options.version.revision, version.revision) .set(options.version.status, ACTIVE) .set(options.version.type, version.type) .set(options.version.branch, version.branch); if (!options.versionTableProperties.isEmpty()) { Map<PropertyPath, Object> properties = version.getProperties(); options.versionTableProperties.forEach((path, column) -> { @SuppressWarnings("unchecked") Path<Object> columnPath = (Path<Object>) column; versionBatch.set(columnPath, properties.get(path)); }); } setMeta(version.getMeta(), versionBatch); versionBatch.addBatch(); } /** * Override to persist custom metadata into VERSION table. * * @see AbstractVersionStoreJdbc#getMeta(Group) */ protected void setMeta(M meta, StoreClause versionBatch) {} protected void insertParents(VersionNode<PropertyPath, Object, M> version) { version.parentRevisions.forEach(parentRevision -> insertParent(version.revision, parentRevision, ACTIVE)); } protected void insertParent(Revision revision, Revision parentRevision, VersionStatus status) { parentBatch .set(options.parent.revision, revision) .set(options.parent.parentRevision, parentRevision) .set(options.parent.status, status) .addBatch(); } protected void insertProperties(VersionNode<PropertyPath, Object, M> version) { version.getChangeset().forEach((path, value) -> insertProperty(version.revision, path, value, ACTIVE)); } protected void insertProperty(Revision revision, PropertyPath path, Object value, VersionStatus status) { if (!options.versionTableProperties.containsKey(path)) { propertyBatch .set(options.property.revision, revision) .set(options.property.path, path.toString()) .set(options.property.status, status); setValue(path, value); propertyBatch.addBatch(); } } protected void setValue(@SuppressWarnings("unused") PropertyPath path, Object value) { // type: // n=null, O=object, A=array, s=string, // b=boolean, l=long, d=double, D=bigdecimal char type; String str = null; Long nbr = null; switch (Persistent.Type.of(value)) { case TOMBSTONE: type = 'n'; break; case NULL: type = 'N'; break; case OBJECT: type = 'O'; str = ((Persistent.Object) value).type; break; case ARRAY: type = 'A'; break; case STRING: type = 's'; str = (String) value; break; case BOOLEAN: type = 'b'; nbr = ((Boolean) value) ? 1L : 0L; break; case LONG: type = 'l'; nbr = (Long) value; break; case DOUBLE: type = 'd'; nbr = Double.doubleToRawLongBits((Double) value); break; case BIG_DECIMAL: type = 'D'; str = value.toString(); break; default: throw new IllegalArgumentException("Unsupported type: " + value.getClass()); } propertyBatch .set(options.property.type, Character.toString(type)) .set(options.property.str, str) .set(options.property.nbr, nbr); } private void insertOptimizedParentsAndProperties(ObjectVersionGraph<M> optimizedGraph) { optimizedGraph.getVersionNodes().forEach(node -> { insertParents(node); insertProperties(node); }); } private void deleteParents(List<Revision> revisions) { options.queryFactory .delete(options.parent) .where(options.parent.revision.in(revisions)) .execute(); } private void deleteProperties(List<Revision> revisions) { options.queryFactory .delete(options.property) .where(options.property.revision.in(revisions)) .execute(); } private void deleteVersions(List<Revision> revisions) { // Delete squashed versions long count = options.queryFactory .delete(options.version) .where(options.version.revision.in(revisions)) .execute(); if (count != revisions.size()) { throw new ConcurrentMaintenanceException("Expected to delete " + revisions.size() + " revisions. Got " + count); } } private void deleteRedundantParents(List<Revision> revisions) { options.queryFactory .delete(options.parent) .where(options.parent.revision.in(revisions), options.parent.status.eq(REDUNDANT)) .execute(); } private void deleteRedundantProperties(List<Revision> revisions) { options.queryFactory .delete(options.property) .where(options.property.revision.in(revisions), options.property.status.eq(REDUNDANT)) .execute(); } private void squashVersions(List<Revision> revisions) { long count = options.queryFactory .update(options.version) .set(options.version.status, SQUASHED) .where(options.version.revision.in(revisions), options.version.status.ne(SQUASHED)) .execute(); if (count != revisions.size()) { throw new ConcurrentMaintenanceException("Expected to squash " + revisions.size() + " revisions. Got " + count); } } private void squashParent(Revision revision, Revision parentRevision) { options.queryFactory .update(options.parent) .set(options.parent.status, SQUASHED) .where(options.parent.revision.eq(revision), options.parent.parentRevision.eq(parentRevision)) .execute(); } private void squashProperty(Revision revision, PropertyPath path) { options.queryFactory .update(options.property) .set(options.property.status, SQUASHED) .where(options.property.revision.eq(revision), options.property.path.eq(path.toString())) .execute(); } }