package org.javersion.store.jdbc; import static com.google.common.collect.Lists.newArrayList; import static java.util.Arrays.asList; import static java.util.UUID.randomUUID; import static org.assertj.core.api.Assertions.assertThat; import static org.javersion.path.PropertyPath.ROOT; import static org.javersion.path.PropertyPath.parse; 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 static org.javersion.store.sql.QDocumentVersion.documentVersion; import static org.javersion.store.sql.QDocumentVersionParent.documentVersionParent; import static org.javersion.store.sql.QDocumentVersionProperty.documentVersionProperty; import java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import javax.annotation.Resource; import org.javersion.core.Persistent; import org.javersion.core.Revision; import org.javersion.core.Version; import org.javersion.core.VersionNode; import org.javersion.object.ObjectVersion; import org.javersion.object.ObjectVersionGraph; import org.javersion.object.ObjectVersionManager; import org.javersion.object.Versionable; import org.javersion.path.PropertyPath; import org.junit.Test; import org.springframework.transaction.support.TransactionTemplate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.querydsl.core.group.GroupBy; import com.querydsl.sql.SQLQueryFactory; public class DocumentVersionStoreJdbcTest extends AbstractVersionStoreTest { @Versionable public static class Price { public final BigDecimal amount; private Price() { this(null); } public Price(BigDecimal amount) { this.amount = amount; } } @Versionable public static class Product { public long id; public String name; public Price price; public List<String> tags; public double vat; public boolean outOfStock; } private final ObjectVersionManager<Product, String> versionManager = new ObjectVersionManager<Product, String>(Product.class).init(); @Resource DocumentVersionStoreJdbc<String, String, JDocumentVersion<String>> documentStore; @Resource DocumentVersionStoreJdbc<String, String, JDocumentVersion<String>> mappedDocumentStore; @Resource TransactionTemplate transactionTemplate; @Resource SQLQueryFactory queryFactory; @Test public void insert_and_load() { String docId = randomUUID().toString(); assertThat(documentStore.getFullGraph(docId).isEmpty()).isTrue(); Product product = new Product(); product.id = 123l; product.name = "product"; ObjectVersion<String> versionOne = versionManager.versionBuilder(product).build(); documentStore.append(docId, versionManager.getVersionNode(versionOne.revision)); assertThat(documentStore.getFullGraph(docId).isEmpty()).isTrue(); documentStore.publish(); ObjectVersionGraph<String> versionGraph = documentStore.getFullGraph(docId); assertThat(versionGraph.isEmpty()).isFalse(); assertThat(versionGraph.getTip().getVersion()).isEqualTo(versionOne); product.price = new Price(new BigDecimal(10)); product.tags = ImmutableList.of("tag", "and", "another"); product.vat = 22.5; documentStore.append(docId, versionManager.versionBuilder(product).buildVersionNode()); product.outOfStock = true; ObjectVersion<String> lastVersion = versionManager.versionBuilder(product).build(); documentStore.append(docId, versionManager.getVersionNode(lastVersion.revision)); assertThat(documentStore.getFullGraph(docId).getTip().getVersion()).isEqualTo(versionOne); documentStore.publish(); versionGraph = documentStore.getFullGraph(docId); assertThat(versionGraph.getTip().getVersion()).isEqualTo(lastVersion); versionManager.init(versionGraph); Product persisted = versionManager.mergeBranches(Version.DEFAULT_BRANCH).object; assertThat(persisted.id).isEqualTo(product.id); assertThat(persisted.name).isEqualTo(product.name); assertThat(persisted.outOfStock).isEqualTo(product.outOfStock); assertThat(persisted.price.amount).isEqualTo(product.price.amount); assertThat(persisted.tags).isEqualTo(product.tags); assertThat(persisted.vat).isEqualTo(product.vat); } @Test public void load_version_with_empty_changeset() { String docId = randomUUID().toString(); ObjectVersion<String> emptyVersion = new ObjectVersion.Builder<String>().build(); ObjectVersionGraph<String> versionGraph = ObjectVersionGraph.init(emptyVersion); documentStore.append(docId, versionGraph.getTip()); documentStore.publish(); versionGraph = documentStore.getFullGraph(docId); List<Version<PropertyPath, Object, String>> versions = newArrayList(versionGraph.getVersions()); assertThat(versions).hasSize(1); assertThat(versions.get(0)).isEqualTo(emptyVersion); } @Test public void ordinal_is_assigned_by_publish() throws InterruptedException { final CountDownLatch firstInsertDone = new CountDownLatch(1); final CountDownLatch secondInsertDone = new CountDownLatch(1); final CountDownLatch firstInsertCommitted = new CountDownLatch(1); final String docId = randomUUID().toString(); final Revision r1 = new Revision(); final Revision r2 = new Revision(); new Thread(() -> { transactionTemplate.execute(status -> { ObjectVersion<String> version1 = ObjectVersion.<String>builder(r1) .changeset(mapOf(ROOT.property("concurrency"), " slow")) .build(); documentStore.append(docId, ObjectVersionGraph.init(version1).getTip()); // First insert is done, but transaction is not committed yet firstInsertDone.countDown(); try { // Wait until second insert is committed before committing this secondInsertDone.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } return null; }); firstInsertCommitted.countDown(); }).start(); // Wait until first insert is done, but not committed yet firstInsertDone.await(); // Insert and commit another before first insert is committed ObjectVersion<String> version2 = ObjectVersion.<String>builder(r2) .changeset(mapOf("concurrency", "fast")) .build(); documentStore.append(docId, ObjectVersionGraph.init(version2).getTip()); documentStore.publish(); // Verify that first insert is not yet visible long count = queryFactory.from(documentVersion) .where(documentVersion.docId.eq(docId)) .fetchCount(); assertThat(count).isEqualTo(1); // Let the first transaction commit secondInsertDone.countDown(); firstInsertCommitted.await(); // Verify that first insert is now visible (committed) count = queryFactory.from(documentVersion) .where(documentVersion.docId.eq(docId)) .fetchCount(); assertThat(count).isEqualTo(2); // Before documentStore.publish(), unpublished version should not have ordinal Map<Revision, Long> ordinals = findOrdinals(docId); assertThat(ordinals.get(r1)).isNull(); assertThat(ordinals.get(r2)).isNotNull(); // documentStore.publish() should assign ordinal documentStore.publish(); ordinals = findOrdinals(docId); assertThat(ordinals.get(r1)).isGreaterThan(ordinals.get(r2)); } private Map<Revision, Long> findOrdinals(String docId) { return queryFactory.from(documentVersion) .where(documentVersion.docId.eq(docId)) .transform(GroupBy.groupBy(documentVersion.revision).as(documentVersion.ordinal)); } @Test public void publish_nothing() { // Flush first if there's pending versions documentStore.publish(); assertThat(documentStore.publish()).isEqualTo(ImmutableMultimap.of()); } @Test public void load_updates() { String docId = randomUUID().toString(); ObjectVersion<String> v1 = ObjectVersion.<String>builder() .changeset(mapOf("property", "value1")) .build(); ObjectVersion<String> v2 = ObjectVersion.<String>builder() .changeset(mapOf("property", "value2")) .build(); ObjectVersionGraph<String> versionGraph = ObjectVersionGraph.init(v1, v2); documentStore.append(docId, versionGraph.getVersionNode(v1.revision)); assertThat(documentStore.publish()).isEqualTo(ImmutableMultimap.of(docId, v1.revision)); // v1 documentStore.append(docId, versionGraph.getVersionNode(v2.revision)); List<ObjectVersion<String>> updates = documentStore.fetchUpdates(docId, v1.revision); assertThat(updates).isEmpty(); assertThat(documentStore.publish()).isEqualTo(ImmutableMultimap.of(docId, v2.revision)); // v2 updates = documentStore.fetchUpdates(docId, v1.revision); assertThat(updates).hasSize(1); assertThat(updates.get(0)).isEqualTo(v2); } /** * v1 * | * v2 * | * v3* * / \ * v4 v5* * | * v6* */ @Test public void prune() { final String docId = randomUUID().toString(); ObjectVersionGraph<String> versionGraph = graphForOptimization(); documentStore.append(docId, ImmutableList.copyOf(versionGraph.getVersionNodes()).reverse()); documentStore.publish(); assertThat(queryFactory.from(documentVersion).where(documentVersion.docId.eq(docId)).fetchCount()).isEqualTo(6); documentStore.prune(docId, graph -> versionNode -> versionNode.revision.equals(rev5) || versionNode.revision.equals(rev6)); assertThat(queryFactory.from(documentVersion).where(documentVersion.docId.eq(docId)).fetchCount()).isEqualTo(3); versionGraph = documentStore.getFullGraph(docId); VersionNode<PropertyPath, Object, String> versionNode = versionGraph.getVersionNode(rev3); assertThat(versionNode.getParentRevisions()).isEmpty(); // Toombstone is removed assertThat(versionNode.getChangeset()).isEqualTo(mapOf("property1", "value1")); assertThat(versionNode.getProperties()).doesNotContainKey(parse("property2")); versionNode = versionGraph.getVersionNode(rev5); assertThat(versionNode.getParentRevisions()).isEqualTo(ImmutableSet.of(rev3)); assertThat(versionNode.getChangeset()).isEqualTo(mapOf("property2", "value2")); versionNode = versionGraph.getVersionNode(rev6); assertThat(versionNode.getParentRevisions()).isEqualTo(ImmutableSet.of(rev3)); assertThat(versionNode.getChangeset()).isEqualTo(mapOf( "property1", "value2", "property2", "value1")); } protected void verifyRedundantRelations() { // Redundant parents of inactive versions are removed assertThat(queryFactory .from(documentVersion) .innerJoin(documentVersion._documentVersionParentParentRevisionFk, documentVersionParent) .where(documentVersion.status.eq(SQUASHED), documentVersionParent.status.eq(REDUNDANT)) .fetchCount()) .isEqualTo(0); // Verify that inverse is true: there exists redundant parents on ACTIVE versions assertThat(queryFactory .from(documentVersion) .innerJoin(documentVersion._documentVersionParentParentRevisionFk, documentVersionParent) .where(documentVersion.status.eq(ACTIVE), documentVersionParent.status.eq(REDUNDANT)) .fetchCount()) .isGreaterThan(0); // Redundant properties of inactive versions are removed assertThat(queryFactory .from(documentVersion) .innerJoin(documentVersion._documentVersionPropertyRevisionFk, documentVersionProperty) .where(documentVersion.status.eq(SQUASHED), documentVersionProperty.status.eq(REDUNDANT)) .fetchCount()) .isEqualTo(0); // Verify that inverse is true: there exists redundant properties on ACTIVE versions assertThat(queryFactory .from(documentVersion) .innerJoin(documentVersion._documentVersionPropertyRevisionFk, documentVersionProperty) .where(documentVersion.status.eq(ACTIVE), documentVersionProperty.status.eq(REDUNDANT)) .fetchCount()) .isGreaterThan(0); } @Test(expected = RuntimeException.class) public void unpublished_version_may_fail_pruning() { String docId = randomUUID().toString(); ObjectVersion<String> v1 = ObjectVersion.<String>builder() .changeset(mapOf("property1", "value1")) .build(); ObjectVersion<String> v2 = ObjectVersion.<String>builder() .changeset(mapOf("property1", "value2")) .parents(v1.revision) .build(); ObjectVersion<String> v3 = ObjectVersion.<String>builder() .changeset(mapOf("property1", "value3")) // v1 is to be squashed... .parents(v1.revision) .build(); ObjectVersionGraph<String> versionGraph = ObjectVersionGraph.init(v1, v2); documentStore.append(docId, ImmutableList.copyOf(versionGraph.getVersionNodes()).reverse()); documentStore.publish(); versionGraph = versionGraph.commit(v3); documentStore.append(docId, versionGraph.getVersionNode(v3.revision)); // v3 is not published yet! documentStore.prune(docId, graph -> v -> v.revision.equals(v2.revision)); } @Test public void supported_value_types() { String docId = randomUUID().toString(); Map<PropertyPath, Object> changeset = mapOf( "Object", Persistent.object("Object"), "Array", Persistent.array(), "String", "String", "Boolean", true, "Long", 123L, "Double", 123.456, "BigDecimal", BigDecimal.TEN, "Null", Persistent.NULL, "Void", null); ObjectVersion<String> v1 = ObjectVersion.<String>builder().changeset(mapOf("Void", "null")).build(), v2 = ObjectVersion.<String>builder().parents(v1.revision).changeset(changeset).build(); ObjectVersionGraph<String> graph = ObjectVersionGraph.init(v1, v2); documentStore.append(docId, graph.getVersionNode(v1.revision)); documentStore.append(docId, graph.getVersionNode(v2.revision)); documentStore.publish(); assertThat(documentStore.getFullGraph(docId).getVersionNode(v2.revision).getVersion()).isEqualTo(v2); } @Test public void load_multiple_documents() { String docId1 = randomUUID().toString(); String docId2 = randomUUID().toString(); Map<PropertyPath, Object> props1 = mapOf("id", docId1); Map<PropertyPath, Object> props2 = mapOf("id", docId2); ObjectVersion<String> v1 = ObjectVersion.<String>builder().changeset(props1).build(); ObjectVersion<String> v2 = ObjectVersion.<String>builder().changeset(props2).build(); documentStore.append(docId1, ObjectVersionGraph.init(v1).getTip()); documentStore.append(docId2, ObjectVersionGraph.init(v2).getTip()); documentStore.publish(); GraphResults<String, String> results = documentStore.getGraphs(asList(docId1, docId2)); assertThat(results.getDocIds()).isEqualTo(ImmutableSet.of(docId1, docId2)); assertThat(results.latestRevision).isEqualTo(v2.revision); assertThat(results.getVersionGraph(docId1).getTip().getVersion()).isEqualTo(v1); assertThat(results.getVersionGraph(docId2).getTip().getVersion()).isEqualTo(v2); } @Test public void id_and_name_mapped_to_version_table() { String docId = randomUUID().toString(); ObjectVersion<String> v1 = ObjectVersion.<String>builder() .changeset(mapOf( "name", "name", "id", 5l)) .build(); mappedDocumentStore.append(docId, ObjectVersionGraph.init(v1).getTip()); mappedDocumentStore.publish(); assertThat(mappedDocumentStore.getFullGraph(docId).getTip().getVersion()).isEqualTo(v1); long count = queryFactory.from(documentVersionProperty) .innerJoin(documentVersionProperty.documentVersionPropertyRevisionFk, documentVersion) .where(documentVersion.docId.eq(docId)) .fetchCount(); assertThat(count).isEqualTo(0); ObjectVersion<String> v2 = ObjectVersion.<String>builder() .parents(v1.revision) .build(); mappedDocumentStore.append(docId, ObjectVersionGraph.init(v1, v2).getTip()); mappedDocumentStore.publish(); // Inherited values count = queryFactory.from(documentVersion) .where(documentVersion.docId.eq(docId), documentVersion.revision.eq(v2.revision), documentVersion.name.eq("name"), documentVersion.id.eq(5l)) .fetchCount(); assertThat(count).isEqualTo(1); assertThat(mappedDocumentStore.getFullGraph(docId).getTip().getVersion()).isEqualTo(v2); } @Override @SuppressWarnings("unchecked") protected AbstractVersionStoreJdbc<String, String, ?, ?, ?> newStore(StoreOptions options) { return new DocumentVersionStoreJdbc<>((DocumentStoreOptions<String, String, JDocumentVersion<String>>) options); } @Override protected AbstractVersionStoreJdbc<String, String, ?, ?, ?> getStore() { return documentStore; } }