All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.projectnessie.versioned.tests.AbstractCommits Maven / Gradle / Ivy

There is a newer version: 0.100.2
Show newest version
/*
 * Copyright (C) 2022 Dremio
 *
 * 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.projectnessie.versioned.tests;

import static com.google.common.collect.Streams.stream;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.assertj.core.api.Assertions.tuple;
import static org.assertj.core.api.InstanceOfAssertFactories.list;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.projectnessie.versioned.VersionStore.KeyRestrictions.NO_KEY_RESTRICTIONS;
import static org.projectnessie.versioned.tests.AbstractCommits.OperationOrder.DELETE_THEN_PUT;
import static org.projectnessie.versioned.tests.AbstractCommits.OperationOrder.PUT_THEN_DELETE;
import static org.projectnessie.versioned.testworker.OnRefOnly.newOnRef;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.assertj.core.api.SoftAssertions;
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions;
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.projectnessie.error.ReferenceConflicts;
import org.projectnessie.model.CommitMeta;
import org.projectnessie.model.Conflict;
import org.projectnessie.model.Conflict.ConflictType;
import org.projectnessie.model.Content;
import org.projectnessie.model.ContentKey;
import org.projectnessie.model.IcebergTable;
import org.projectnessie.versioned.BranchName;
import org.projectnessie.versioned.Commit;
import org.projectnessie.versioned.CommitResult;
import org.projectnessie.versioned.Delete;
import org.projectnessie.versioned.GetNamedRefsParams;
import org.projectnessie.versioned.Hash;
import org.projectnessie.versioned.KeyEntry;
import org.projectnessie.versioned.Operation;
import org.projectnessie.versioned.Put;
import org.projectnessie.versioned.ReferenceAlreadyExistsException;
import org.projectnessie.versioned.ReferenceConflictException;
import org.projectnessie.versioned.ReferenceInfo;
import org.projectnessie.versioned.ReferenceNotFoundException;
import org.projectnessie.versioned.Unchanged;
import org.projectnessie.versioned.VersionStore;
import org.projectnessie.versioned.VersionStore.CommitValidator;
import org.projectnessie.versioned.paging.PaginationIterator;
import org.projectnessie.versioned.testworker.OnRefOnly;

@ExtendWith(SoftAssertionsExtension.class)
public abstract class AbstractCommits extends AbstractNestedVersionStore {
  @InjectSoftAssertions protected SoftAssertions soft;

  private static final OnRefOnly V_1_1 = newOnRef("v1_1");
  private static final OnRefOnly V_1_2 = newOnRef("v1_2");
  private static final OnRefOnly V_1_3 = newOnRef("v1_3");
  private static final OnRefOnly V_2_1 = newOnRef("v2_1");
  private static final OnRefOnly V_2_2 = newOnRef("v2_2");
  private static final OnRefOnly V_3_1 = newOnRef("v3_1");
  private static final OnRefOnly V_3_2 = newOnRef("v3_2");
  private static final OnRefOnly V_4_1 = newOnRef("v4_1");
  private static final OnRefOnly NEW_v2_1 = newOnRef("new_v2_1");

  protected AbstractCommits(VersionStore store) {
    super(store);
  }

  /*
   * Test:
   * - Create a new branch
   * - Add a commit to it
   * - Check that another commit with no operations can be added with the initial hash
   * - Check the commit can be listed
   * - Check that the commit can be deleted
   */
  @Test
  public void commitToBranch() throws Exception {
    final BranchName branch = BranchName.of("foo");

    final Hash createHash = store().create(branch, Optional.empty()).getHash();
    final Hash initialHash = store().hashOnReference(branch, Optional.empty(), emptyList());
    soft.assertThat(createHash).isEqualTo(initialHash);

    CommitResult result =
        store()
            .commit(
                branch,
                Optional.of(initialHash),
                CommitMeta.fromMessage("Some commit"),
                emptyList());
    soft.assertThat(result.getCommit().getParentHash()).isEqualTo(initialHash);
    soft.assertThat(result.getCommit().getOperations()).isNullOrEmpty();
    soft.assertThat(result.getTargetBranch()).isEqualTo(branch);

    final Hash commitHash0 = result.getCommitHash();
    final Hash commitHash = store().hashOnReference(branch, Optional.empty(), emptyList());
    soft.assertThat(commitHash).isEqualTo(commitHash0);

    soft.assertThat(commitHash).isNotEqualTo(initialHash);
    result =
        store()
            .commit(
                branch,
                Optional.of(initialHash),
                CommitMeta.fromMessage("Another commit"),
                emptyList());
    soft.assertThat(result.getCommit().getParentHash()).isEqualTo(commitHash);
    soft.assertThat(result.getCommit().getOperations()).isNullOrEmpty();
    soft.assertThat(result.getTargetBranch()).isEqualTo(branch);

    final Hash anotherCommitHash = store().hashOnReference(branch, Optional.empty(), emptyList());

    soft.assertThat(commitsList(branch, false))
        .contains(
            commit(anotherCommitHash, "Another commit", commitHash),
            commit(commitHash, "Some commit", initialHash));
    soft.assertThat(commitsList(commitHash, false))
        .contains(commit(commitHash, "Some commit", initialHash));

    soft.assertThatThrownBy(() -> store().delete(branch, initialHash))
        .isInstanceOf(ReferenceConflictException.class);

    store().delete(branch, anotherCommitHash);
    soft.assertThatThrownBy(() -> store().hashOnReference(branch, Optional.empty(), emptyList()))
        .isInstanceOf(ReferenceNotFoundException.class);
    try (PaginationIterator> str =
        store().getNamedRefs(GetNamedRefsParams.DEFAULT, null)) {
      soft.assertThat(stream(str).filter(this::filterMainBranch)).isEmpty();
    }
    soft.assertThatThrownBy(() -> store().delete(branch, commitHash))
        .isInstanceOf(ReferenceNotFoundException.class);
  }

  /*
   * Test:
   * - Create a new branch
   * - Add 3 commits in succession with no conflicts to it with put and delete operations
   * - Check commit metadata
   * - Check keys for each commit hash
   * - Check values for each commit hash
   */
  @Test
  public void commitSomeOperations() throws Exception {
    BranchName branch = BranchName.of("foo");

    ContentKey keyT1 = ContentKey.of("t1");
    ContentKey keyT2 = ContentKey.of("t2");
    ContentKey keyT3 = ContentKey.of("t3");
    ContentKey keyT4 = ContentKey.of("t4");

    Hash base = store().create(branch, Optional.empty()).getHash();

    Hash initialCommit =
        commit("Initial Commit")
            .put("t1", V_1_1)
            .put("t2", V_2_1)
            .put("t3", V_3_1)
            .toBranch(branch);
    Content t1 = requireNonNull(store().getValue(branch, ContentKey.of("t1"), false).content());

    Hash secondCommit =
        commit("Second Commit")
            .put("t1", V_1_2.withId(t1.getId()))
            .delete("t2")
            .delete("t3")
            .put("t4", V_4_1)
            .toBranch(branch);

    Hash thirdCommit = commit("Third Commit").put("t2", V_2_2).unchanged("t4").toBranch(branch);

    soft.assertThat(commitsList(branch, false))
        .contains(
            commit(thirdCommit, "Third Commit", secondCommit),
            commit(secondCommit, "Second Commit", initialCommit),
            commit(initialCommit, "Initial Commit", base));

    try (PaginationIterator keys =
        store().getKeys(branch, null, false, NO_KEY_RESTRICTIONS)) {
      soft.assertThat(stream(keys).map(e -> e.getKey().contentKey()))
          .containsExactlyInAnyOrder(keyT1, keyT2, keyT4);
    }

    try (PaginationIterator keys =
        store().getKeys(secondCommit, null, false, NO_KEY_RESTRICTIONS)) {
      soft.assertThat(stream(keys).map(e -> e.getKey().contentKey()))
          .containsExactlyInAnyOrder(keyT1, keyT4);
    }

    try (PaginationIterator keys =
        store().getKeys(initialCommit, null, false, NO_KEY_RESTRICTIONS)) {
      soft.assertThat(stream(keys).map(e -> e.getKey().contentKey()))
          .containsExactlyInAnyOrder(keyT1, keyT2, keyT3);
    }

    soft.assertThat(
            contentsWithoutId(
                store().getValues(secondCommit, Arrays.asList(keyT1, keyT2, keyT3, keyT4), false)))
        .containsExactlyInAnyOrderEntriesOf(ImmutableMap.of(keyT1, V_1_2, keyT4, V_4_1));

    soft.assertThat(
            contentsWithoutId(
                store().getValues(initialCommit, Arrays.asList(keyT1, keyT2, keyT3, keyT4), false)))
        .containsExactlyInAnyOrderEntriesOf(
            ImmutableMap.of(
                keyT1, V_1_1,
                keyT2, V_2_1,
                keyT3, V_3_1));

    soft.assertThat(contentWithoutId(store().getValue(branch, keyT1, false))).isEqualTo(V_1_2);
    soft.assertThat(contentWithoutId(store().getValue(branch, keyT2, false))).isEqualTo(V_2_2);
    soft.assertThat(store().getValue(branch, keyT3, false)).isNull();
    soft.assertThat(contentWithoutId(store().getValue(branch, keyT4, false))).isEqualTo(V_4_1);

    soft.assertThat(contentWithoutId(store().getValue(secondCommit, keyT1, false)))
        .isEqualTo(V_1_2);
    soft.assertThat(store().getValue(secondCommit, keyT2, false)).isNull();
    soft.assertThat(store().getValue(secondCommit, keyT3, false)).isNull();
    soft.assertThat(contentWithoutId(store().getValue(secondCommit, keyT4, false)))
        .isEqualTo(V_4_1);

    soft.assertThat(contentWithoutId(store().getValue(initialCommit, keyT1, false)))
        .isEqualTo(V_1_1);
    soft.assertThat(contentWithoutId(store().getValue(initialCommit, keyT2, false)))
        .isEqualTo(V_2_1);
    soft.assertThat(contentWithoutId(store().getValue(initialCommit, keyT3, false)))
        .isEqualTo(V_3_1);
    soft.assertThat(store().getValue(initialCommit, keyT4, false)).isNull();
  }

  /*
   * Test:
   * - Create a new branch
   * - Add a commit for 3 keys
   * - Add a commit based on initial commit for first key
   * - Add a commit based on initial commit for second key
   * - Add a commit based on initial commit for third  key
   * - Check commit metadata
   * - Check keys for each commit hash
   * - Check values for each commit hash
   */
  @Test
  public void commitNonConflictingOperations() throws Exception {
    BranchName branch = BranchName.of("foo");

    Hash base = store().create(branch, Optional.empty()).getHash();

    Hash initialCommit =
        commit("Initial Commit")
            .put("t1", V_1_1)
            .put("t2", V_2_1)
            .put("t3", V_3_1)
            .toBranch(branch);
    Content t1 = requireNonNull(store().getValue(branch, ContentKey.of("t1"), false).content());
    Content t3 = requireNonNull(store().getValue(branch, ContentKey.of("t3"), false).content());

    Hash t1Commit =
        commit("T1 Commit")
            .fromReference(initialCommit)
            .put("t1", V_1_2.withId(t1.getId()))
            .toBranch(branch);
    t1 = requireNonNull(store().getValue(branch, ContentKey.of("t1"), false).content());

    Hash t2Commit = commit("T2 Commit").fromReference(initialCommit).delete("t2").toBranch(branch);
    Hash t3Commit =
        commit("T3 Commit").fromReference(initialCommit).unchanged("t3").toBranch(branch);
    Hash extraCommit =
        commit("Extra Commit")
            .fromReference(t1Commit)
            .put("t1", V_1_3.withId(t1.getId()))
            .put("t3", V_3_2.withId(t3.getId()))
            .toBranch(branch);
    Hash newT2Commit =
        commit("New T2 Commit").fromReference(t2Commit).put("t2", NEW_v2_1).toBranch(branch);

    soft.assertThat(commitsList(branch, false))
        .contains(
            commit(newT2Commit, "New T2 Commit", extraCommit),
            commit(extraCommit, "Extra Commit", t3Commit),
            commit(t3Commit, "T3 Commit", t2Commit),
            commit(t2Commit, "T2 Commit", t1Commit),
            commit(t1Commit, "T1 Commit", initialCommit),
            commit(initialCommit, "Initial Commit", base));

    try (PaginationIterator keys =
        store().getKeys(branch, null, false, NO_KEY_RESTRICTIONS)) {
      soft.assertThat(stream(keys).map(e -> e.getKey().contentKey()))
          .containsExactlyInAnyOrder(ContentKey.of("t1"), ContentKey.of("t2"), ContentKey.of("t3"));
    }

    soft.assertThat(
            contentsWithoutId(
                store()
                    .getValues(
                        branch,
                        Arrays.asList(
                            ContentKey.of("t1"), ContentKey.of("t2"), ContentKey.of("t3")),
                        false)))
        .containsExactlyInAnyOrderEntriesOf(
            ImmutableMap.of(
                ContentKey.of("t1"), V_1_3,
                ContentKey.of("t2"), NEW_v2_1,
                ContentKey.of("t3"), V_3_2));

    soft.assertThat(
            contentsWithoutId(
                store()
                    .getValues(
                        newT2Commit,
                        Arrays.asList(
                            ContentKey.of("t1"), ContentKey.of("t2"), ContentKey.of("t3")),
                        false)))
        .containsExactlyInAnyOrderEntriesOf(
            ImmutableMap.of(
                ContentKey.of("t1"), V_1_3,
                ContentKey.of("t2"), NEW_v2_1,
                ContentKey.of("t3"), V_3_2));

    soft.assertThat(
            contentsWithoutId(
                store()
                    .getValues(
                        extraCommit,
                        Arrays.asList(
                            ContentKey.of("t1"), ContentKey.of("t2"), ContentKey.of("t3")),
                        false)))
        .containsExactlyInAnyOrderEntriesOf(
            ImmutableMap.of(
                ContentKey.of("t1"), V_1_3,
                ContentKey.of("t3"), V_3_2));

    soft.assertThat(
            contentsWithoutId(
                store()
                    .getValues(
                        t3Commit,
                        Arrays.asList(
                            ContentKey.of("t1"), ContentKey.of("t2"), ContentKey.of("t3")),
                        false)))
        .containsExactlyInAnyOrderEntriesOf(
            ImmutableMap.of(
                ContentKey.of("t1"), V_1_2,
                ContentKey.of("t3"), V_3_1));

    soft.assertThat(
            contentsWithoutId(
                store()
                    .getValues(
                        t2Commit,
                        Arrays.asList(
                            ContentKey.of("t1"), ContentKey.of("t2"), ContentKey.of("t3")),
                        false)))
        .containsExactlyInAnyOrderEntriesOf(
            ImmutableMap.of(
                ContentKey.of("t1"), V_1_2,
                ContentKey.of("t3"), V_3_1));

    soft.assertThat(
            contentsWithoutId(
                store()
                    .getValues(
                        t1Commit,
                        Arrays.asList(
                            ContentKey.of("t1"), ContentKey.of("t2"), ContentKey.of("t3")),
                        false)))
        .containsExactlyInAnyOrderEntriesOf(
            ImmutableMap.of(
                ContentKey.of("t1"), V_1_2,
                ContentKey.of("t2"), V_2_1,
                ContentKey.of("t3"), V_3_1));
  }

  /*
   * Test:
   * - Create a new branch
   * - Add a commit to create 2 keys
   * - Add a second commit to delete one key and add a new one
   * - Check that put operations against 1st commit for the 3 keys fail
   * - Check that delete operations against 1st commit for the 3 keys fail
   * - Check that unchanged operations against 1st commit for the 3 keys fail
   * - Check that branch state hasn't changed
   */
  @Test
  public void commitConflictingOperations() throws Exception {
    BranchName branch = BranchName.of("foo");

    store().create(branch, Optional.empty());

    Hash initialCommit =
        commit("Initial Commit").put("t1", V_1_1).put("t2", V_2_1).toBranch(branch);

    Content t1 = requireNonNull(store().getValue(branch, ContentKey.of("t1"), false).content());
    Content t2 = requireNonNull(store().getValue(branch, ContentKey.of("t2"), false).content());

    Hash secondCommit =
        commit("Second Commit")
            .put("t1", V_1_2.withId(t1.getId()))
            .delete("t2")
            .put("t3", V_3_1)
            .toBranch(branch);
    Content t3 = requireNonNull(store().getValue(branch, ContentKey.of("t3"), false).content());

    soft.assertThatThrownBy(
            () ->
                commit("Conflicting Commit")
                    .fromReference(initialCommit)
                    .put("t1", V_1_3)
                    .toBranch(branch))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessage("New value to update existing key 't1' has no content ID");
    soft.assertThatThrownBy(
            () ->
                commit("Conflicting Commit")
                    .fromReference(initialCommit)
                    .put("t4", V_4_1)
                    .unchanged("t2")
                    .toBranch(branch))
        .isInstanceOf(ReferenceConflictException.class)
        .hasMessageEndingWith("Key 't2' does not exist.")
        .asInstanceOf(type(ReferenceConflictException.class))
        .extracting(ReferenceConflictException::getReferenceConflicts)
        .extracting(ReferenceConflicts::conflicts, list(Conflict.class))
        .extracting(Conflict::conflictType, Conflict::key, Conflict::message)
        .containsExactly(
            tuple(ConflictType.KEY_DOES_NOT_EXIST, ContentKey.of("t2"), "key 't2' does not exist"));
    soft.assertThatThrownBy(
            () ->
                commit("Conflicting Commit")
                    .fromReference(initialCommit)
                    .put("t4", V_4_1)
                    .unchanged("t1")
                    .toBranch(branch))
        .isInstanceOf(ReferenceConflictException.class)
        .hasMessageEndingWith("Values of existing and expected content for key 't1' are different.")
        .asInstanceOf(type(ReferenceConflictException.class))
        .extracting(ReferenceConflictException::getReferenceConflicts)
        .extracting(ReferenceConflicts::conflicts, list(Conflict.class))
        .extracting(Conflict::conflictType, Conflict::key, Conflict::message)
        .containsExactly(
            tuple(
                ConflictType.VALUE_DIFFERS,
                ContentKey.of("t1"),
                "values of existing and expected content for key 't1' are different"));
    soft.assertThatThrownBy(
            () ->
                commit("Conflicting Commit")
                    .fromReference(initialCommit)
                    .put("t1", V_1_3.withId(t1.getId()))
                    .toBranch(branch))
        .isInstanceOf(ReferenceConflictException.class)
        .hasMessageEndingWith("Values of existing and expected content for key 't1' are different.")
        .asInstanceOf(type(ReferenceConflictException.class))
        .extracting(ReferenceConflictException::getReferenceConflicts)
        .extracting(ReferenceConflicts::conflicts, list(Conflict.class))
        .extracting(Conflict::conflictType, Conflict::key, Conflict::message)
        .containsExactly(
            tuple(
                ConflictType.VALUE_DIFFERS,
                ContentKey.of("t1"),
                "values of existing and expected content for key 't1' are different"));
    soft.assertThatThrownBy(
            () ->
                commit("Conflicting Commit")
                    .fromReference(initialCommit)
                    .put("t2", V_2_2.withId(t2.getId()))
                    .toBranch(branch))
        .isInstanceOf(ReferenceConflictException.class)
        .hasMessageEndingWith("Key 't2' does not exist.")
        .asInstanceOf(type(ReferenceConflictException.class))
        .extracting(ReferenceConflictException::getReferenceConflicts)
        .extracting(ReferenceConflicts::conflicts, list(Conflict.class))
        .extracting(Conflict::conflictType, Conflict::key, Conflict::message)
        .containsExactly(
            tuple(ConflictType.KEY_DOES_NOT_EXIST, ContentKey.of("t2"), "key 't2' does not exist"));
    soft.assertThatThrownBy(
            () ->
                commit("Conflicting Commit")
                    .fromReference(initialCommit)
                    .put("t3", V_3_2.withId(t3.getId()))
                    .toBranch(branch))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining("New value for key 't3' must not have a content ID");
    soft.assertThatThrownBy(
            () ->
                commit("Conflicting Commit")
                    .fromReference(initialCommit)
                    .put("t3", V_3_2)
                    .toBranch(branch))
        .isInstanceOf(ReferenceConflictException.class)
        .hasMessageEndingWith("Key 't3' already exists.")
        .asInstanceOf(type(ReferenceConflictException.class))
        .extracting(ReferenceConflictException::getReferenceConflicts)
        .extracting(ReferenceConflicts::conflicts, list(Conflict.class))
        .extracting(Conflict::conflictType, Conflict::key, Conflict::message)
        .containsExactly(
            tuple(ConflictType.KEY_EXISTS, ContentKey.of("t3"), "key 't3' already exists"));

    soft.assertThatThrownBy(
            () ->
                commit("Conflicting Commit")
                    .fromReference(initialCommit)
                    .delete("t1")
                    .toBranch(branch))
        .isInstanceOf(ReferenceConflictException.class)
        .hasMessageEndingWith("Values of existing and expected content for key 't1' are different.")
        .asInstanceOf(type(ReferenceConflictException.class))
        .extracting(ReferenceConflictException::getReferenceConflicts)
        .extracting(ReferenceConflicts::conflicts, list(Conflict.class))
        .extracting(Conflict::conflictType, Conflict::key, Conflict::message)
        .containsExactly(
            tuple(
                ConflictType.VALUE_DIFFERS,
                ContentKey.of("t1"),
                "values of existing and expected content for key 't1' are different"));
    soft.assertThatThrownBy(
            () ->
                commit("Conflicting Commit")
                    .fromReference(initialCommit)
                    .delete("t2")
                    .toBranch(branch))
        .isInstanceOf(ReferenceConflictException.class)
        .hasMessageEndingWith("Key 't2' does not exist.")
        .asInstanceOf(type(ReferenceConflictException.class))
        .extracting(ReferenceConflictException::getReferenceConflicts)
        .extracting(ReferenceConflicts::conflicts, list(Conflict.class))
        .extracting(Conflict::conflictType, Conflict::key, Conflict::message)
        .containsExactly(
            tuple(ConflictType.KEY_DOES_NOT_EXIST, ContentKey.of("t2"), "key 't2' does not exist"));
    soft.assertThatThrownBy(
            () ->
                commit("Conflicting Commit")
                    .fromReference(initialCommit)
                    .delete("t3")
                    .toBranch(branch))
        .isInstanceOf(ReferenceConflictException.class)
        .hasMessageEndingWith(
            "Payload of existing and expected content for key 't3' are different.")
        .asInstanceOf(type(ReferenceConflictException.class))
        .extracting(ReferenceConflictException::getReferenceConflicts)
        .extracting(ReferenceConflicts::conflicts, list(Conflict.class))
        .extracting(Conflict::conflictType, Conflict::key, Conflict::message)
        .containsExactly(
            tuple(
                ConflictType.PAYLOAD_DIFFERS,
                ContentKey.of("t3"),
                "payload of existing and expected content for key 't3' are different"));

    // Checking the state hasn't changed
    soft.assertThat(store().hashOnReference(branch, Optional.empty(), emptyList()))
        .isEqualTo(secondCommit);
  }

  /*
   * Test:
   * - Create a new branch
   * - Add a commit to create 2 keys
   * - Add a second commit to delete one key and add a new one
   * - force commit put operations
   * - Check that put operations against 1st commit for the 3 keys fail
   * - Check that delete operations against 1st commit for the 3 keys fail
   * - Check that unchanged operations against 1st commit for the 3 keys fail
   * - Check that branch state hasn't changed
   */
  @Test
  public void forceCommitConflictingOperations() throws Exception {
    BranchName branch = BranchName.of("foo");

    store().create(branch, Optional.empty());

    commit("Initial Commit").put("t1", V_1_1).put("t2", V_2_1).toBranch(branch);
    Content t1 = requireNonNull(store().getValue(branch, ContentKey.of("t1"), false).content());

    commit("Second Commit")
        .put("t1", V_1_2.withId(t1.getId()))
        .delete("t2")
        .put("t3", V_3_1)
        .toBranch(branch);
    Content t3 = requireNonNull(store().getValue(branch, ContentKey.of("t3"), false).content());

    Hash putCommit =
        forceCommit("Conflicting Commit")
            .put("t1", V_1_3.withId(t1.getId()))
            .put("t2", V_2_2)
            .put("t3", V_3_2.withId(t3.getId()))
            .toBranch(branch);

    soft.assertThat(store().hashOnReference(branch, Optional.empty(), emptyList()))
        .isEqualTo(putCommit);
    soft.assertThat(
            contentsWithoutId(
                store()
                    .getValues(
                        branch,
                        Arrays.asList(
                            ContentKey.of("t1"), ContentKey.of("t2"), ContentKey.of("t3")),
                        false)))
        .containsExactlyInAnyOrderEntriesOf(
            ImmutableMap.of(
                ContentKey.of("t1"), V_1_3,
                ContentKey.of("t2"), V_2_2,
                ContentKey.of("t3"), V_3_2));

    Hash unchangedCommit =
        commit("Conflicting Commit")
            .unchanged("t1")
            .unchanged("t2")
            .unchanged("t3")
            .toBranch(branch);
    soft.assertThat(store().hashOnReference(branch, Optional.empty(), emptyList()))
        .isEqualTo(unchangedCommit);
    soft.assertThat(
            contentsWithoutId(
                store()
                    .getValues(
                        branch,
                        Arrays.asList(
                            ContentKey.of("t1"), ContentKey.of("t2"), ContentKey.of("t3")),
                        false)))
        .containsExactlyInAnyOrderEntriesOf(
            ImmutableMap.of(
                ContentKey.of("t1"), V_1_3,
                ContentKey.of("t2"), V_2_2,
                ContentKey.of("t3"), V_3_2));

    Hash deleteCommit =
        commit("Conflicting Commit").delete("t1").delete("t2").delete("t3").toBranch(branch);
    soft.assertThat(store().hashOnReference(branch, Optional.empty(), emptyList()))
        .isEqualTo(deleteCommit);
    soft.assertThat(
            store()
                .getValues(
                    branch,
                    Arrays.asList(ContentKey.of("t1"), ContentKey.of("t2"), ContentKey.of("t3")),
                    false))
        .isEmpty();
  }

  /*
   * Test:
   *  - Check that store allows storing the same value under different keys
   */
  @Test
  public void commitDuplicateValues() throws Exception {
    BranchName branch = BranchName.of("dupe-values");
    store().create(branch, Optional.empty());
    OnRefOnly foo1 = newOnRef("foo");
    OnRefOnly foo2 = newOnRef("foo");
    store()
        .commit(
            branch,
            Optional.empty(),
            CommitMeta.fromMessage("metadata"),
            ImmutableList.of(put("keyA", foo1), put("keyB", foo2)));

    soft.assertThat(contentWithoutId(store().getValue(branch, ContentKey.of("keyA"), false)))
        .isEqualTo(foo1);
    soft.assertThat(contentWithoutId(store().getValue(branch, ContentKey.of("keyB"), false)))
        .isEqualTo(foo2);
  }

  /*
   * Test:
   * - Check that store throws RNFE if branch doesn't exist
   */
  @Test
  public void commitWithInvalidBranch() {
    final BranchName branch = BranchName.of("unknown");

    soft.assertThatThrownBy(
            () ->
                store()
                    .commit(
                        branch,
                        Optional.empty(),
                        CommitMeta.fromMessage("New commit"),
                        emptyList()))
        .isInstanceOf(ReferenceNotFoundException.class);
  }

  /*
   * Test:
   * - Check that store throws RNFE if reference hash doesn't exist
   */
  @Test
  public void commitWithUnknownReference()
      throws ReferenceNotFoundException, ReferenceAlreadyExistsException {
    BranchName branch = BranchName.of("foo");
    store().create(branch, Optional.empty());

    soft.assertThatThrownBy(
            () ->
                store()
                    .commit(
                        branch,
                        Optional.of(Hash.of("1234567890abcdef")),
                        CommitMeta.fromMessage("New commit"),
                        emptyList()))
        .isInstanceOf(ReferenceNotFoundException.class);
  }

  /*
   * Test:
   * - Check that store throws IllegalArgumentException if reference hash is not in branch ancestry
   */
  @Test
  public void commitWithInvalidReference()
      throws ReferenceNotFoundException,
          ReferenceConflictException,
          ReferenceAlreadyExistsException {
    BranchName branch = BranchName.of("foo");
    Hash initialHash = store().create(branch, Optional.empty()).getHash();

    Hash commitHash =
        store()
            .commit(
                branch,
                Optional.of(initialHash),
                CommitMeta.fromMessage("Some commit"),
                emptyList())
            .getCommitHash();

    BranchName branch2 = BranchName.of("bar");
    store().create(branch2, Optional.empty());

    soft.assertThatThrownBy(
            () ->
                store()
                    .commit(
                        branch2,
                        Optional.of(commitHash),
                        CommitMeta.fromMessage("Another commit"),
                        emptyList()))
        .isInstanceOf(ReferenceNotFoundException.class);
  }

  enum OperationOrder {
    PUT_THEN_DELETE,
    DELETE_THEN_PUT
  }

  static Stream renames() {
    return Stream.of(
        arguments(DELETE_THEN_PUT, false, false),
        arguments(DELETE_THEN_PUT, false, true),
        arguments(DELETE_THEN_PUT, true, false),
        arguments(DELETE_THEN_PUT, true, true),
        arguments(PUT_THEN_DELETE, false, false),
        arguments(PUT_THEN_DELETE, false, true),
        arguments(PUT_THEN_DELETE, true, false),
        arguments(PUT_THEN_DELETE, true, true));
  }

  @ParameterizedTest
  @MethodSource("renames")
  void renames(OperationOrder order, boolean reuseContentKey, boolean reuseContentId)
      throws Exception {
    BranchName branch = BranchName.of("foo");
    Hash initialHash = store().create(branch, Optional.empty()).getHash();

    ContentKey original = ContentKey.of("original");

    Hash committed =
        store()
            .commit(
                branch,
                Optional.of(initialHash),
                CommitMeta.fromMessage("Some commit"),
                Collections.singletonList(Put.of(original, IcebergTable.of("loc", 1, 2, 3, 4))))
            .getCommitHash();

    Content table = requireNonNull(store().getValue(branch, original, false).content());

    Delete deleteOp = Delete.of(original);

    ContentKey renamed = reuseContentKey ? original : ContentKey.of("renamed");

    Put putOp =
        Put.of(
            renamed,
            reuseContentId
                ? IcebergTable.of("loc", 1, 2, 3, 4, table.getId())
                : IcebergTable.of("loc", 1, 2, 3, 4));

    List ops =
        order == PUT_THEN_DELETE ? Arrays.asList(putOp, deleteOp) : Arrays.asList(deleteOp, putOp);

    Throwable error =
        catchThrowable(
            () ->
                store()
                    .commit(
                        branch,
                        Optional.of(committed),
                        CommitMeta.fromMessage("Rename commit"),
                        ops));

    if (reuseContentKey) {
      if (order == DELETE_THEN_PUT && !reuseContentId) {
        // re-add (DELETE + PUT with same key but without id) allowed with new storage
        soft.assertThat(error).isNull();
      } else {
        soft.assertThat(error)
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Duplicate key");
      }
      soft.assertThat(store().getValues(branch, Collections.singletonList(original), false))
          .containsKey(original);
    } else {
      soft.assertThat(error).isNull();
      soft.assertThat(store().getValues(branch, Arrays.asList(original, renamed), false))
          .containsKey(renamed)
          .doesNotContainKey(original);
    }
  }

  @Test
  void renameTwice() throws Exception {
    BranchName branch = BranchName.of("main");

    ContentKey key = ContentKey.of("table");
    ContentKey keyBackup = ContentKey.of("table_backup");
    ContentKey keyTemp = ContentKey.of("table_tmp");
    List keys = ImmutableList.of(key, keyTemp, keyBackup);

    IcebergTable tableOld = IcebergTable.of("old", 1, 2, 3, 4);
    IcebergTable tableNew = IcebergTable.of("new", 1, 2, 3, 4);

    store()
        .commit(
            branch,
            Optional.empty(),
            CommitMeta.fromMessage("commit"),
            singletonList(Put.of(key, tableOld)))
        .getCommitHash();
    tableOld = (IcebergTable) requireNonNull(store().getValue(branch, key, false).content());

    soft.assertThat(
            store().getValues(branch, keys, false).entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().content())))
        .hasSize(1)
        .containsEntry(key, tableOld);

    // create new "table_tmp"

    store()
        .commit(
            branch,
            Optional.empty(),
            CommitMeta.fromMessage("new"),
            singletonList(Put.of(keyTemp, tableNew)));
    tableNew = (IcebergTable) requireNonNull(store().getValue(branch, keyTemp, false).content());

    soft.assertThat(
            store().getValues(branch, keys, false).entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().content())))
        .hasSize(2)
        .containsEntry(key, tableOld)
        .containsEntry(keyTemp, tableNew);

    // rename "original" to "original_backup"

    store()
        .commit(
            branch,
            Optional.empty(),
            CommitMeta.fromMessage("backup"),
            ImmutableList.of(Delete.of(key), Put.of(keyBackup, tableOld)));

    soft.assertThat(
            store().getValues(branch, keys, false).entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().content())))
        .hasSize(2)
        .containsEntry(keyBackup, tableOld)
        .containsEntry(keyTemp, tableNew);

    // rename new "table_tmp" to "table"

    store()
        .commit(
            branch,
            Optional.empty(),
            CommitMeta.fromMessage("rename new"),
            ImmutableList.of(Delete.of(keyTemp), Put.of(key, tableNew)));

    soft.assertThat(
            store().getValues(branch, keys, false).entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().content())))
        .hasSize(2)
        .containsEntry(keyBackup, tableOld)
        .containsEntry(key, tableNew);

    // delete backup "table_backup"

    store()
        .commit(
            branch,
            Optional.empty(),
            CommitMeta.fromMessage("delete"),
            singletonList(Delete.of(keyBackup)));

    soft.assertThat(
            store().getValues(branch, keys, false).entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().content())))
        .hasSize(1)
        .containsEntry(key, tableNew);
  }

  @Test
  void commitWithValidation() throws Exception {
    BranchName branch = BranchName.of("main");
    ContentKey key = ContentKey.of("table0");
    Hash branchHead = store().getNamedRef(branch.getName(), GetNamedRefsParams.DEFAULT).getHash();

    RuntimeException exception = new ArithmeticException("Whatever");
    soft.assertThatThrownBy(
            () ->
                doCommitWithValidation(
                    branch,
                    key,
                    validation -> {
                      // do some operations here
                      try {
                        assertThat(store().getValue(branch, key, false)).isNull();
                        store().getKeys(branch, null, false, NO_KEY_RESTRICTIONS).close();
                      } catch (ReferenceNotFoundException e) {
                        throw new RuntimeException(e);
                      }

                      // let the custom commit-validation fail
                      throw exception;
                    }))
        .isSameAs(exception);

    soft.assertThat(store().getNamedRef(branch.getName(), GetNamedRefsParams.DEFAULT).getHash())
        .isEqualTo(branchHead);
    soft.assertThat(store().getValue(branch, key, false)).isNull();
  }

  void doCommitWithValidation(BranchName branch, ContentKey key, CommitValidator validator)
      throws Exception {
    store()
        .commit(
            branch,
            Optional.empty(),
            CommitMeta.fromMessage("initial commit meta"),
            Collections.singletonList(Put.of(key, newOnRef("some value"))),
            validator,
            (k, c) -> {});
  }

  static Stream duplicateKeys() {
    ContentKey key = ContentKey.of("my.awesome.table");
    String tableRefState = "table ref state";
    Content createValue1 = newOnRef("no no - not this");
    Content createValue2 = newOnRef(tableRefState);
    return Stream.of(
        Arguments.of(Put.of(key, createValue1), Put.of(key, createValue2)),
        Arguments.of(Put.of(key, createValue2), Delete.of(key)), // PUT-DELETE is not re-add!
        Arguments.of(Put.of(key, createValue2), Unchanged.of(key)),
        // Arguments.of(Delete.of(key), Put.of(key, createValue2)), // re-add => allowed
        Arguments.of(Delete.of(key), Delete.of(key)),
        Arguments.of(Delete.of(key), Unchanged.of(key)),
        Arguments.of(Unchanged.of(key), Put.of(key, createValue2)),
        Arguments.of(Unchanged.of(key), Delete.of(key)),
        Arguments.of(Unchanged.of(key), Unchanged.of(key)));
  }

  @ParameterizedTest
  @MethodSource("duplicateKeys")
  void duplicateKeys(Operation operation1, Operation operation2) {
    BranchName branch = BranchName.of("main");
    ContentKey key = ContentKey.of("my.awesome.table");
    soft.assertThatThrownBy(
            () ->
                store()
                    .commit(
                        branch,
                        Optional.empty(),
                        CommitMeta.fromMessage("initial"),
                        Arrays.asList(operation1, operation2)))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining(key.toString());
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy