org.neo4j.kernel.api.impl.index.LuceneSchemaIndexUniquenessVerificationIT.java Source code

Java tutorial

Introduction

Here is the source code for org.neo4j.kernel.api.impl.index.LuceneSchemaIndexUniquenessVerificationIT.java

Source

/*
 * Copyright (c) 2002-2016 "Neo Technology,"
 * Network Engine for Objects in Lund AB [http://neotechnology.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.neo4j.kernel.api.impl.index;

import org.apache.commons.lang3.RandomStringUtils;
import org.apache.lucene.document.Document;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.IntStream;

import org.neo4j.helpers.ArrayUtil;
import org.neo4j.helpers.Exceptions;
import org.neo4j.helpers.Strings;
import org.neo4j.io.IOUtils;
import org.neo4j.kernel.api.exceptions.index.IndexEntryConflictException;
import org.neo4j.kernel.api.impl.schema.LuceneDocumentStructure;
import org.neo4j.kernel.api.impl.schema.LuceneSchemaIndex;
import org.neo4j.kernel.api.impl.schema.LuceneSchemaIndexBuilder;
import org.neo4j.kernel.api.index.PreexistingIndexEntryConflictException;
import org.neo4j.kernel.api.index.PropertyAccessor;
import org.neo4j.kernel.api.properties.Property;
import org.neo4j.test.Randoms;
import org.neo4j.test.TargetDirectory;

import static java.util.stream.Collectors.toSet;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;

@RunWith(Parameterized.class)
public class LuceneSchemaIndexUniquenessVerificationIT {
    private static final int DOCS_PER_PARTITION = ThreadLocalRandom.current().nextInt(10, 100);
    private static final int PROPERTY_KEY_ID = 42;

    @Rule
    public TargetDirectory.TestDirectory testDir = TargetDirectory.testDirForTest(getClass());

    @Parameter
    public int nodesToCreate;

    private LuceneSchemaIndex index;

    @Parameters(name = "created nodes: {0}")
    public static Iterable<Object[]> data() {
        return Arrays.asList(new Object[][] { { DOCS_PER_PARTITION / 2 }, { DOCS_PER_PARTITION / 2 + 1 },
                { DOCS_PER_PARTITION / 3 }, { DOCS_PER_PARTITION / 3 + 1 }, { DOCS_PER_PARTITION * 2 },
                { DOCS_PER_PARTITION * 2 + 1 }, { DOCS_PER_PARTITION * 3 }, { DOCS_PER_PARTITION * 3 + 1 } });
    }

    @Before
    public void setPartitionSize() throws Exception {
        System.setProperty("luceneSchemaIndex.maxPartitionSize", String.valueOf(DOCS_PER_PARTITION));

        index = LuceneSchemaIndexBuilder.create().uniqueIndex()
                .withIndexRootFolder(testDir.directory("uniquenessVerification")).withIndexIdentifier("index")
                .build();

        index.create();
        index.open();
    }

    @After
    public void resetPartitionSize() throws IOException {
        System.setProperty("luceneSchemaIndex.maxPartitionSize", "");

        IOUtils.closeAll(index);
    }

    @Test
    public void stringValuesWithoutDuplicates() throws IOException {
        Set<PropertyValue> data = randomStrings();

        insert(data);

        assertUniquenessConstraintHolds(data);
    }

    @Test
    public void stringValuesWithDuplicates() throws IOException {
        List<PropertyValue> data = withDuplicate(randomStrings());

        insert(data);

        assertUniquenessConstraintFails(data);
    }

    @Test
    public void smallLongValuesWithoutDuplicates() throws IOException {
        long min = randomLongInRange(100, 10_000);
        long max = min + nodesToCreate;
        Set<PropertyValue> data = randomLongs(min, max);

        insert(data);

        assertUniquenessConstraintHolds(data);
    }

    @Test
    public void smallLongValuesWithDuplicates() throws IOException {
        long min = randomLongInRange(100, 10_000);
        long max = min + nodesToCreate;
        List<PropertyValue> data = withDuplicate(randomLongs(min, max));

        insert(data);

        assertUniquenessConstraintFails(data);
    }

    @Test
    public void largeLongValuesWithoutDuplicates() throws IOException {
        long max = randomLongInRange(Long.MAX_VALUE - 20, Long.MAX_VALUE);
        long min = max - nodesToCreate;
        Set<PropertyValue> data = randomLongs(min, max);

        insert(data);

        assertUniquenessConstraintHolds(data);
    }

    @Test
    public void largeLongValuesWithDuplicates() throws IOException {
        long max = randomLongInRange(Long.MAX_VALUE - 20, Long.MAX_VALUE);
        long min = max - nodesToCreate;
        List<PropertyValue> data = withDuplicate(randomLongs(min, max));

        insert(data);

        assertUniquenessConstraintFails(data);
    }

    @Test
    public void smallDoubleValuesWithoutDuplicates() throws IOException {
        double min = randomDoubleInRange(100, 10_000);
        double max = min + nodesToCreate;
        Set<PropertyValue> data = randomDoubles(min, max);

        insert(data);

        assertUniquenessConstraintHolds(data);
    }

    @Test
    public void smallDoubleValuesWithDuplicates() throws IOException {
        double min = randomDoubleInRange(100, 10_000);
        double max = min + nodesToCreate;
        List<PropertyValue> data = withDuplicate(randomDoubles(min, max));

        insert(data);

        assertUniquenessConstraintFails(data);
    }

    @Test
    public void largeDoubleValuesWithoutDuplicates() throws IOException {
        double max = randomDoubleInRange(Double.MAX_VALUE / 2, Double.MAX_VALUE);
        double min = max / 2;
        Set<PropertyValue> data = randomDoubles(min, max);

        insert(data);

        assertUniquenessConstraintHolds(data);
    }

    @Test
    public void largeDoubleValuesWithDuplicates() throws IOException {
        double max = randomDoubleInRange(Double.MAX_VALUE / 2, Double.MAX_VALUE);
        double min = max / 2;
        List<PropertyValue> data = withDuplicate(randomDoubles(min, max));

        insert(data);

        assertUniquenessConstraintFails(data);
    }

    @Test
    public void smallArrayValuesWithoutDuplicates() throws IOException {
        Set<PropertyValue> data = randomArrays(3, 7);

        insert(data);

        assertUniquenessConstraintHolds(data);
    }

    @Test
    public void smallArrayValuesWithDuplicates() throws IOException {
        List<PropertyValue> data = withDuplicate(randomArrays(3, 7));

        insert(data);

        assertUniquenessConstraintFails(data);
    }

    @Test
    public void largeArrayValuesWithoutDuplicates() throws IOException {
        Set<PropertyValue> data = randomArrays(70, 100);

        insert(data);

        assertUniquenessConstraintHolds(data);
    }

    @Test
    public void largeArrayValuesWithDuplicates() throws IOException {
        List<PropertyValue> data = withDuplicate(randomArrays(70, 100));

        insert(data);

        assertUniquenessConstraintFails(data);
    }

    @Test
    public void variousValuesWithoutDuplicates() throws IOException {
        Set<PropertyValue> data = randomPropertyValues();

        insert(data);

        assertUniquenessConstraintHolds(data);
    }

    @Test
    public void variousValuesWitDuplicates() throws IOException {
        List<PropertyValue> data = withDuplicate(randomPropertyValues());

        insert(data);

        assertUniquenessConstraintFails(data);
    }

    private void insert(Collection<PropertyValue> data) throws IOException {
        PropertyValue[] dataArray = data.toArray(new PropertyValue[data.size()]);
        for (int i = 0; i < dataArray.length; i++) {
            Document doc = LuceneDocumentStructure.documentRepresentingProperty(i, dataArray[i].value);
            index.getIndexWriter().addDocument(doc);
        }
        index.maybeRefreshBlocking();
    }

    private void assertUniquenessConstraintHolds(Collection<PropertyValue> data) {
        try {
            verifyUniqueness(data);
        } catch (Throwable t) {
            fail("Unable to create uniqueness constraint for data: " + Strings.prettyPrint(data.toArray()) + "\n"
                    + Exceptions.stringify(t));
        }
    }

    private void assertUniquenessConstraintFails(Collection<PropertyValue> data) {
        try {
            verifyUniqueness(data);
            fail("Should not be possible to create uniqueness constraint for data: "
                    + Strings.prettyPrint(data.toArray()));
        } catch (Throwable t) {
            assertThat(t, instanceOf(PreexistingIndexEntryConflictException.class));
        }
    }

    private void verifyUniqueness(Collection<PropertyValue> data) throws IOException, IndexEntryConflictException {
        Object[] propertyValues = data.stream().map(property -> property.value).toArray();
        PropertyAccessor propertyAccessor = new TestPropertyAccessor(propertyValues);
        index.verifyUniqueness(propertyAccessor, PROPERTY_KEY_ID);
    }

    private Set<PropertyValue> randomStrings() {
        return ThreadLocalRandom.current().ints(nodesToCreate, 1, 200).mapToObj(this::randomString)
                .map(PropertyValue::new).collect(toSet());
    }

    private String randomString(int size) {
        return ThreadLocalRandom.current().nextBoolean() ? RandomStringUtils.random(size)
                : RandomStringUtils.randomAlphabetic(size);
    }

    private Set<PropertyValue> randomLongs(long min, long max) {
        return ThreadLocalRandom.current().longs(nodesToCreate, min, max).boxed().map(PropertyValue::new)
                .collect(toSet());
    }

    private Set<PropertyValue> randomDoubles(double min, double max) {
        return ThreadLocalRandom.current().doubles(nodesToCreate, min, max).boxed().map(PropertyValue::new)
                .collect(toSet());
    }

    private Set<PropertyValue> randomArrays(int minLength, int maxLength) {
        Randoms randoms = new Randoms(ThreadLocalRandom.current(), new ArraySizeConfig(minLength, maxLength));

        return IntStream.range(0, nodesToCreate).mapToObj(i -> randoms.array()).map(PropertyValue::new)
                .collect(toSet());
    }

    private Set<PropertyValue> randomPropertyValues() {
        Randoms randoms = new Randoms(ThreadLocalRandom.current(), new ArraySizeConfig(5, 100));

        return IntStream.range(0, nodesToCreate).mapToObj(i -> randoms.propertyValue()).map(PropertyValue::new)
                .collect(toSet());
    }

    private static List<PropertyValue> withDuplicate(Set<PropertyValue> set) {
        List<PropertyValue> data = new ArrayList<>(set);
        if (data.isEmpty()) {
            throw new IllegalStateException();
        } else if (data.size() == 1) {
            data.add(data.get(0));
        } else {
            int duplicateIndex = (int) randomLongInRange(0, data.size());
            int duplicateValueIndex;
            do {
                duplicateValueIndex = ThreadLocalRandom.current().nextInt(data.size());
            } while (duplicateValueIndex == duplicateIndex);
            PropertyValue duplicateValue = data.get(duplicateValueIndex);
            data.set(duplicateIndex, duplicateValue);
        }
        return data;
    }

    private static long randomLongInRange(long min, long max) {
        return ThreadLocalRandom.current().nextLong(min, max);
    }

    private static double randomDoubleInRange(double min, double max) {
        return ThreadLocalRandom.current().nextDouble(min, max);
    }

    private static class ArraySizeConfig extends Randoms.Default {
        final int minLength;
        final int maxLength;

        ArraySizeConfig(int minLength, int maxLength) {
            this.minLength = minLength;
            this.maxLength = maxLength;
        }

        @Override
        public int arrayMinLength() {
            return super.arrayMinLength();
        }

        @Override
        public int arrayMaxLength() {
            return super.arrayMaxLength();
        }
    }

    /**
     * This class is used to implement correct equals and hashCode for property values of numeric and array types.
     */
    private static class PropertyValue {
        final Object value;

        PropertyValue(Object value) {
            this.value = Objects.requireNonNull(value);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            PropertyValue that = (PropertyValue) o;
            return Property.property(PROPERTY_KEY_ID, value).valueEquals(that.value);
        }

        @Override
        public int hashCode() {
            if (value instanceof Number) {
                return Double.hashCode(((Number) value).doubleValue());
            } else if (value.getClass().isArray()) {
                return ArrayUtil.hashCode(value);
            }
            return value.hashCode();
        }

        @Override
        public String toString() {
            return Strings.prettyPrint(value);
        }
    }
}