Package org.lilyproject.repository.impl.test

Source Code of org.lilyproject.repository.impl.test.AbstractRepositoryTest

/*
* Copyright 2010 Outerthought bvba
*
* 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.lilyproject.repository.impl.test;

import static org.easymock.EasyMock.createControl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import org.apache.hadoop.hbase.util.Bytes;
import org.easymock.IMocksControl;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.lilyproject.bytes.api.ByteArray;
import org.lilyproject.repository.api.Blob;
import org.lilyproject.repository.api.CompareOp;
import org.lilyproject.repository.api.ConcurrentRecordUpdateException;
import org.lilyproject.repository.api.FieldNotFoundException;
import org.lilyproject.repository.api.FieldType;
import org.lilyproject.repository.api.IdGenerator;
import org.lilyproject.repository.api.IdRecord;
import org.lilyproject.repository.api.IdRecordScanner;
import org.lilyproject.repository.api.InvalidRecordException;
import org.lilyproject.repository.api.Link;
import org.lilyproject.repository.api.Metadata;
import org.lilyproject.repository.api.MetadataBuilder;
import org.lilyproject.repository.api.MutationCondition;
import org.lilyproject.repository.api.QName;
import org.lilyproject.repository.api.Record;
import org.lilyproject.repository.api.RecordBuilder;
import org.lilyproject.repository.api.RecordException;
import org.lilyproject.repository.api.RecordExistsException;
import org.lilyproject.repository.api.RecordId;
import org.lilyproject.repository.api.RecordNotFoundException;
import org.lilyproject.repository.api.RecordScan;
import org.lilyproject.repository.api.RecordScanner;
import org.lilyproject.repository.api.RecordType;
import org.lilyproject.repository.api.RecordTypeNotFoundException;
import org.lilyproject.repository.api.Repository;
import org.lilyproject.repository.api.RepositoryException;
import org.lilyproject.repository.api.ResponseStatus;
import org.lilyproject.repository.api.ReturnFields;
import org.lilyproject.repository.api.SchemaId;
import org.lilyproject.repository.api.Scope;
import org.lilyproject.repository.api.TypeManager;
import org.lilyproject.repository.api.ValueType;
import org.lilyproject.repository.api.VersionNotFoundException;
import org.lilyproject.repository.api.filter.FieldValueFilter;
import org.lilyproject.repository.api.filter.RecordFilterList;
import org.lilyproject.repository.api.filter.RecordIdPrefixFilter;
import org.lilyproject.repository.api.filter.RecordTypeFilter;
import org.lilyproject.repository.api.filter.RecordVariantFilter;
import org.lilyproject.repotestfw.RepositorySetup;
import org.lilyproject.util.Pair;

public abstract class AbstractRepositoryTest {

    protected static final RepositorySetup repoSetup = new RepositorySetup();
    protected static boolean avro = false;

    protected static IdGenerator idGenerator;
    protected static TypeManager typeManager;
    protected static Repository repository;
    protected static FieldType fieldType1;

    private static FieldType fieldType1B;
    private static FieldType fieldType2;
    private static FieldType fieldType3;
    private static FieldType fieldType4;
    private static FieldType fieldType5;
    private static FieldType fieldType6;
    protected static RecordType recordType1;
    private static RecordType recordType1B;
    private static RecordType recordType2;
    private static RecordType recordType3;
    private static String namespace = "/test/repository";


    @Before
    public void setUp() throws Exception {
    }

    @After
    public void tearDown() throws Exception {
    }

    protected static void setupTypes() throws Exception {
        setupFieldTypes();
        setupRecordTypes();
    }

    private static void setupFieldTypes() throws Exception {
        fieldType1 = typeManager.createFieldType(typeManager
                .newFieldType(typeManager.getValueType("STRING"), new QName(namespace, "field1"), Scope.NON_VERSIONED));
        fieldType1B = typeManager.createFieldType(typeManager
                .newFieldType(typeManager.getValueType("STRING"), new QName(namespace, "field1B"),
                        Scope.NON_VERSIONED));
        fieldType2 = typeManager.createFieldType(typeManager
                .newFieldType(typeManager.getValueType("INTEGER"), new QName(namespace, "field2"), Scope.VERSIONED));
        fieldType3 = typeManager.createFieldType(
                typeManager.newFieldType(typeManager.getValueType("BOOLEAN"), new QName(namespace, "field3"),
                        Scope.VERSIONED_MUTABLE));

        fieldType4 = typeManager.createFieldType(typeManager
                .newFieldType(typeManager.getValueType("INTEGER"), new QName(namespace, "field4"),
                        Scope.NON_VERSIONED));
        fieldType5 = typeManager.createFieldType(typeManager
                .newFieldType(typeManager.getValueType("BOOLEAN"), new QName(namespace, "field5"), Scope.VERSIONED));
        fieldType6 = typeManager.createFieldType(typeManager
                .newFieldType(typeManager.getValueType("STRING"), new QName(namespace, "field6"),
                        Scope.VERSIONED_MUTABLE));

    }

    private static void setupRecordTypes() throws Exception {
        recordType1 = typeManager.newRecordType(new QName(namespace, "RT1"));
        recordType1.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType1.getId(), false));
        recordType1.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType2.getId(), false));
        recordType1.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType3.getId(), false));
        recordType1 = typeManager.createRecordType(recordType1);

        recordType1B = recordType1.clone();
        recordType1B.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType1B.getId(), false));
        recordType1B = typeManager.updateRecordType(recordType1B);

        recordType2 = typeManager.newRecordType(new QName(namespace, "RT2"));

        recordType2.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType4.getId(), false));
        recordType2.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType5.getId(), false));
        recordType2.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType6.getId(), false));
        recordType2 = typeManager.createRecordType(recordType2);

        recordType3 = typeManager.newRecordType(new QName(namespace, "RT3"));
        recordType3.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType1.getId(), false));
        recordType3.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType2.getId(), false));
        recordType3.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType3.getId(), false));
        recordType3 = typeManager.createRecordType(recordType3);
        recordType3.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType1.getId(), true));
        recordType3 = typeManager.updateRecordType(recordType3);
        recordType3.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType3.getId(), true));
        recordType3 = typeManager.updateRecordType(recordType3);
        recordType3.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType6.getId(), false));
        recordType3 = typeManager.updateRecordType(recordType3);


    }


    @Test
    public void testRecordCreateWithoutRecordType() throws Exception {
        IMocksControl control = createControl();
        control.replay();
        Record record = repository.newRecord(idGenerator.newRecordId());
        try {
            if (avro) {
                System.out.println("Expecting InvalidRecordException");
            }
            record = repository.create(record);
            fail();
        } catch (InvalidRecordException expected) {
        }
        control.verify();
    }

    @Test
    public void testEmptyRecordCreate() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        try {
            if (avro) {
                System.out.println("Expecting InvalidRecordException");
            }
            record = repository.create(record);
            fail();
        } catch (InvalidRecordException expected) {
        }
    }

    @Test
    public void testCreate() throws Exception {
        IMocksControl control = createControl();
        control.replay();
        Record createdRecord = createDefaultRecord();

        assertEquals(Long.valueOf(1), createdRecord.getVersion());
        assertEquals("value1", createdRecord.getField(fieldType1.getName()));
        assertEquals(123, createdRecord.getField(fieldType2.getName()));
        assertTrue((Boolean) createdRecord.getField(fieldType3.getName()));
        assertEquals(recordType1.getName(), createdRecord.getRecordTypeName());
        assertEquals(Long.valueOf(1), createdRecord.getRecordTypeVersion());
        assertEquals(recordType1.getName(), createdRecord.getRecordTypeName(Scope.NON_VERSIONED));
        assertEquals(Long.valueOf(1), createdRecord.getRecordTypeVersion(Scope.NON_VERSIONED));
        assertEquals(recordType1.getName(), createdRecord.getRecordTypeName(Scope.VERSIONED));
        assertEquals(Long.valueOf(1), createdRecord.getRecordTypeVersion(Scope.VERSIONED));
        assertEquals(recordType1.getName(), createdRecord.getRecordTypeName(Scope.VERSIONED_MUTABLE));
        assertEquals(Long.valueOf(1), createdRecord.getRecordTypeVersion(Scope.VERSIONED_MUTABLE));

        assertEquals(createdRecord, repository.read(createdRecord.getId()));
        control.verify();
    }

    @Test
    public void testCreateTwice() throws Exception {
        IMocksControl control = createControl();
        control.replay();
        Record createdRecord = createDefaultRecord();

        Record record = repository.newRecord(createdRecord.getId());
        record.setRecordType(recordType1.getName(), recordType1.getVersion());
        record.setField(fieldType1.getName(), "value1");
        record.setField(fieldType2.getName(), 123);
        record.setField(fieldType3.getName(), true);
        try {
            repository.create(record);
            fail();
        } catch (RecordExistsException expected) {
        }

        control.verify();
    }

    @Test
    public void testCreateNoVersions() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName(), recordType1.getVersion());
        record.setField(fieldType1.getName(), "value1");

        record = repository.create(record);
        assertEquals(null, record.getVersion());
    }

    @Test
    public void testCreateOnlyVersionAndCheckRecordType() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName(), recordType1.getVersion());
        record.setField(fieldType2.getName(), 123);

        record = repository.create(record);

        Record readRecord = repository.read(record.getId());
        // Check that the 'global' record type of the read record is also filled in
        assertEquals(recordType1.getName(), readRecord.getRecordTypeName());
        assertEquals(recordType1.getVersion(), readRecord.getRecordTypeVersion());

        // The record type for the versioned scope (only field present) should be returned
        assertEquals(recordType1.getName(), readRecord.getRecordTypeName(Scope.VERSIONED));
        assertEquals(recordType1.getVersion(), readRecord.getRecordTypeVersion(Scope.VERSIONED));

        // The record type for the version-mutable scope should not be returned since no such field is present
        assertEquals(null, readRecord.getRecordTypeName(Scope.VERSIONED_MUTABLE));
        assertEquals(null, readRecord.getRecordTypeVersion(Scope.VERSIONED_MUTABLE));
    }

    protected Record createDefaultRecord() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName(), recordType1.getVersion());
        record.setField(fieldType1.getName(), "value1");
        record.setField(fieldType2.getName(), 123);
        record.setField(fieldType3.getName(), true);
        return repository.create(record);
    }

    @Test
    public void testCreateWithNonExistingRecordTypeFails() throws Exception {
        Record record = repository.newRecord(idGenerator.newRecordId());
        record.setRecordType(new QName("foo", "bar"));
        record.setField(fieldType1.getName(), "value1");
        try {
            if (avro) {
                System.out.println("Expecting RecordTypeNotFoundException");
            }
            repository.create(record);
            fail();
        } catch (RecordTypeNotFoundException expected) {
        }
    }

    @Test
    public void testCreateUsesLatestRecordType() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        record.setField(fieldType1.getName(), "value1");
        Record createdRecord = repository.create(record);
        assertEquals(recordType1.getName(), createdRecord.getRecordTypeName());
        assertEquals(Long.valueOf(2), createdRecord.getRecordTypeVersion());
        assertEquals(recordType1.getName(), createdRecord.getRecordTypeName(Scope.NON_VERSIONED));
        assertEquals(Long.valueOf(2), createdRecord.getRecordTypeVersion(Scope.NON_VERSIONED));
        assertNull(createdRecord.getRecordTypeName(Scope.VERSIONED));
        assertNull(createdRecord.getRecordTypeVersion(Scope.VERSIONED));
        assertNull(createdRecord.getRecordTypeName(Scope.VERSIONED_MUTABLE));
        assertNull(createdRecord.getRecordTypeVersion(Scope.VERSIONED_MUTABLE));

        assertEquals(createdRecord, repository.read(createdRecord.getId()));
    }

    @Test
    public void testCreateVariant() throws Exception {
        Record record = createDefaultRecord();

        Map<String, String> variantProperties = new HashMap<String, String>();
        variantProperties.put("dimension1", "dimval1");
        Record variant = repository.newRecord(idGenerator.newRecordId(record.getId(), variantProperties));
        variant.setRecordType(recordType1.getName());
        variant.setField(fieldType1.getName(), "value2");
        variant.setField(fieldType2.getName(), 567);
        variant.setField(fieldType3.getName(), false);

        Record createdVariant = repository.create(variant);

        assertEquals(Long.valueOf(1), createdVariant.getVersion());
        assertEquals("value2", createdVariant.getField(fieldType1.getName()));
        assertEquals(567, createdVariant.getField(fieldType2.getName()));
        assertFalse((Boolean) createdVariant.getField(fieldType3.getName()));

        assertEquals(createdVariant, repository.read(variant.getId()));

        Set<RecordId> variants = repository.getVariants(record.getId());
        assertEquals(2, variants.size());
        assertTrue(variants.contains(record.getId()));
        assertTrue(variants.contains(createdVariant.getId()));
    }

    @Test
    public void testUpdate() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = record.cloneRecord();
        updateRecord.setField(fieldType1.getName(), "value2");
        updateRecord.setField(fieldType2.getName(), 789);
        updateRecord.setField(fieldType3.getName(), false);

        Record updatedRecord = repository.update(updateRecord);

        assertEquals(Long.valueOf(2), updatedRecord.getVersion());
        assertEquals("value2", updatedRecord.getField(fieldType1.getName()));
        assertEquals(789, updatedRecord.getField(fieldType2.getName()));
        assertEquals(false, updatedRecord.getField(fieldType3.getName()));

        assertEquals(updatedRecord, repository.read(record.getId()));
    }

    @Test
    public void testUpdateWithoutRecordType() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = repository.newRecord(record.getId());
        updateRecord.setField(fieldType1.getName(), "value2");
        updateRecord.setField(fieldType2.getName(), 789);
        updateRecord.setField(fieldType3.getName(), false);

        Record updatedRecord = repository.update(updateRecord);

        assertEquals(record.getRecordTypeName(), updatedRecord.getRecordTypeName());
        assertEquals(Long.valueOf(2), updatedRecord.getRecordTypeVersion());

        assertEquals(Long.valueOf(2), updatedRecord.getVersion());
        assertEquals("value2", updatedRecord.getField(fieldType1.getName()));
        assertEquals(789, updatedRecord.getField(fieldType2.getName()));
        assertEquals(false, updatedRecord.getField(fieldType3.getName()));

        assertEquals(updatedRecord, repository.read(record.getId()));
    }

    @Test
    public void testUpdateOnlyOneField() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = repository.newRecord(record.getId());
        updateRecord.setRecordType(record.getRecordTypeName(), record.getRecordTypeVersion());
        updateRecord.setField(fieldType1.getName(), "value2");

        Record updatedRecord = repository.update(updateRecord);

        assertEquals(Long.valueOf(1), updatedRecord.getVersion());
        assertEquals("value2", updatedRecord.getField(fieldType1.getName()));
        try {
            if (avro) {
                System.out.println("Expecting FieldNotFoundException");
            }
            updatedRecord.getField(fieldType2.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }
        try {
            if (avro) {
                System.out.println("Expecting FieldNotFoundException");
            }
            updatedRecord.getField(fieldType3.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }

        Record readRecord = repository.read(record.getId());
        assertEquals("value2", readRecord.getField(fieldType1.getName()));
        assertEquals(123, readRecord.getField(fieldType2.getName()));
        assertEquals(true, readRecord.getField(fieldType3.getName()));
    }

    @Test
    public void testEmptyUpdate() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = repository.newRecord(record.getId());
        updateRecord.setRecordType(record.getRecordTypeName(), record.getRecordTypeVersion());

        Record updatedRecord = repository.update(updateRecord);

        assertEquals(Long.valueOf(1), updatedRecord.getVersion());
        try {
            if (avro) {
                System.out.println("Expecting FieldNotFoundException");
            }
            updatedRecord.getField(fieldType1.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }
        try {
            if (avro) {
                System.out.println("Expecting FieldNotFoundException");
            }
            updatedRecord.getField(fieldType2.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }
        try {
            if (avro) {
                System.out.println("Expecting FieldNotFoundException");
            }
            updatedRecord.getField(fieldType3.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }

        assertEquals(record, repository.read(record.getId()));
    }

    @Test
    public void testIdempotentUpdate() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = record.cloneRecord();

        Record updatedRecord = repository.update(updateRecord);

        assertEquals(Long.valueOf(1), updatedRecord.getVersion());
        assertEquals("value1", updatedRecord.getField(fieldType1.getName()));
        assertEquals(123, updatedRecord.getField(fieldType2.getName()));
        assertEquals(true, updatedRecord.getField(fieldType3.getName()));

        assertEquals(record, repository.read(record.getId()));
    }

    @Test
    public void testUpdateIgnoresVersion() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = record.cloneRecord();
        updateRecord.setVersion(Long.valueOf(99));
        updateRecord.setField(fieldType1.getName(), "value2");

        Record updatedRecord = repository.update(updateRecord);

        assertEquals(Long.valueOf(1), updatedRecord.getVersion());

        assertEquals(updatedRecord, repository.read(record.getId()));
    }

    @Test
    public void testUpdateNonVersionable() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = repository.newRecord(record.getId());
        updateRecord.setRecordType(record.getRecordTypeName());
        updateRecord.setField(fieldType1.getName(), "aNewValue");
        repository.update(updateRecord);

        Record readRecord = repository.read(record.getId());
        assertEquals(Long.valueOf(1), readRecord.getVersion());
        assertEquals("aNewValue", readRecord.getField(fieldType1.getName()));
    }

    @Test
    public void testReadOlderVersions() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = record.cloneRecord();
        updateRecord.setField(fieldType1.getName(), "value2");
        updateRecord.setField(fieldType2.getName(), 789);
        updateRecord.setField(fieldType3.getName(), false);

        // This update will use the latest version of the RecordType
        // I.e. version2 of recordType1 instead of version 1
        repository.update(updateRecord);

        record.setRecordType(recordType1B.getName(), recordType1B.getVersion());
        record.setField(fieldType1.getName(), "value2");
        assertEquals(record, repository.read(record.getId(), 1L));

        try {
            if (avro) {
                System.out.println("Expecting RecordNotFoundException");
            }
            repository.read(record.getId(), 0L);
            fail();
        } catch (RecordNotFoundException expected) {
        }
    }

    @Test
    public void testReadAllVersions() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = record.cloneRecord();
        updateRecord.setField(fieldType1.getName(), "value2");
        updateRecord.setField(fieldType2.getName(), 789);
        updateRecord.setField(fieldType3.getName(), false);

        repository.update(updateRecord);

        List<Record> list = repository.readVersions(record.getId(), 1L, 2L, (QName[]) null);
        assertEquals(2, list.size());
        assertTrue(list.contains(repository.read(record.getId(), 1L)));
        assertTrue(list.contains(repository.read(record.getId(), 2L)));
    }

    @Test
    public void testReadVersionsWideBoundaries() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = record.cloneRecord();
        updateRecord.setField(fieldType1.getName(), "value2");
        updateRecord.setField(fieldType2.getName(), 789);
        updateRecord.setField(fieldType3.getName(), false);

        repository.update(updateRecord);

        List<Record> list = repository.readVersions(record.getId(), 0L, 5L, (QName[]) null);
        assertEquals(2, list.size());
        assertTrue(list.contains(repository.read(record.getId(), 1L)));
        assertTrue(list.contains(repository.read(record.getId(), 2L)));
    }

    @Test
    public void testReadVersionsNarrowBoundaries() throws Exception {
        Record record = createDefaultRecord();

        Record updateRecord = record.cloneRecord();
        updateRecord.setField(fieldType1.getName(), "value2");
        updateRecord.setField(fieldType2.getName(), 789);
        updateRecord.setField(fieldType3.getName(), false);
        repository.update(updateRecord);

        updateRecord = record.cloneRecord();
        updateRecord.setField(fieldType2.getName(), 790);
        repository.update(updateRecord);

        updateRecord = record.cloneRecord();
        updateRecord.setField(fieldType2.getName(), 791);
        repository.update(updateRecord);

        List<Record> list = repository.readVersions(record.getId(), 2L, 3L);
        assertEquals(2, list.size());
        assertTrue(list.contains(repository.read(record.getId(), 2L)));
        assertTrue(list.contains(repository.read(record.getId(), 3L)));
    }

    @Test
    public void testReadSpecificVersions() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = record.cloneRecord();
        updateRecord.setField(fieldType1.getName(), "value2");
        updateRecord.setField(fieldType2.getName(), 789);
        // Don't update this field, as a test that the internal version inheritance code works correctly
        // updateRecord.setField(fieldType3.getName(), false);

        repository.update(updateRecord);

        // Now update this field again
        updateRecord.setField(fieldType3.getName(), false);
        repository.update(updateRecord);

        Record record1 = repository.read(record.getId(), 1L);
        Record record2 = repository.read(record.getId(), 2L);
        Record record3 = repository.read(record.getId(), 3L);

        List<Record> records = repository.readVersions(record.getId(), Arrays.asList(1L, 2L, 3L), (QName[]) null);
        assertEquals(3, records.size());
        assertTrue(records.contains(record1));
        assertTrue(records.contains(record2));
        assertTrue(records.contains(record3));

        records = repository.readVersions(record.getId(), new ArrayList<Long>(), (QName[]) null);
        assertEquals(0, records.size());

        records = repository.readVersions(record.getId(), Arrays.asList(1L, 5L), (QName[]) null);
        assertEquals(1, records.size());
        assertTrue(records.contains(record1));
    }

    @Test
    public void testReadNonExistingRecord() throws Exception {
        try {
            if (avro) {
                System.out.println("Expecting RecordNotFoundException");
            }
            repository.read(idGenerator.newRecordId());
            fail();
        } catch (RecordNotFoundException expected) {
        }
    }

    @Test
    public void testReadTooRecentRecord() throws Exception {
        Record record = createDefaultRecord();
        try {
            if (avro) {
                System.out.println("Expecting VersionNotFoundException");
            }
            repository.read(record.getId(), Long.valueOf(2));
            fail();
        } catch (VersionNotFoundException expected) {
        }
    }

    @Test
    public void testReadSpecificFields() throws Exception {
        Record record = createDefaultRecord();
        Record readRecord =
                repository.read(record.getId(), fieldType1.getName(), fieldType2.getName(), fieldType3.getName());
        assertEquals(repository.read(record.getId()), readRecord);
    }

    @Test
    public void testUpdateWithNewRecordTypeVersion() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = repository.newRecord(record.getId());
        updateRecord.setRecordType(recordType1B.getName(), recordType1B.getVersion());
        updateRecord.setField(fieldType1.getName(), "value2");
        updateRecord.setField(fieldType2.getName(), 789);
        updateRecord.setField(fieldType3.getName(), false);

        Record updatedRecord = repository.update(updateRecord);
        assertEquals(recordType1B.getName(), updatedRecord.getRecordTypeName());
        assertEquals(recordType1B.getVersion(), updatedRecord.getRecordTypeVersion());
        assertEquals(recordType1B.getName(), updatedRecord.getRecordTypeName(Scope.NON_VERSIONED));
        assertEquals(recordType1B.getVersion(), updatedRecord.getRecordTypeVersion(Scope.NON_VERSIONED));
        assertEquals(recordType1B.getName(), updatedRecord.getRecordTypeName(Scope.VERSIONED));
        assertEquals(recordType1B.getVersion(), updatedRecord.getRecordTypeVersion(Scope.VERSIONED));
        assertEquals(recordType1B.getName(), updatedRecord.getRecordTypeName(Scope.VERSIONED_MUTABLE));
        assertEquals(recordType1B.getVersion(), updatedRecord.getRecordTypeVersion(Scope.VERSIONED_MUTABLE));

        Record recordV1 = repository.read(record.getId(), Long.valueOf(1));
        assertEquals(recordType1B.getName(), recordV1.getRecordTypeName());
        assertEquals(recordType1B.getVersion(), recordV1.getRecordTypeVersion());
        assertEquals(recordType1B.getName(), recordV1.getRecordTypeName(Scope.NON_VERSIONED));
        assertEquals(recordType1B.getVersion(), recordV1.getRecordTypeVersion(Scope.NON_VERSIONED));
        assertEquals(recordType1.getName(), recordV1.getRecordTypeName(Scope.VERSIONED));
        assertEquals(recordType1.getVersion(), recordV1.getRecordTypeVersion(Scope.VERSIONED));
        assertEquals(recordType1.getName(), recordV1.getRecordTypeName(Scope.VERSIONED_MUTABLE));
        assertEquals(recordType1.getVersion(), recordV1.getRecordTypeVersion(Scope.VERSIONED_MUTABLE));
    }

    @Test
    public void testUpdateWithNewRecordTypeVersionWithoutOtherChanges() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = repository.newRecord(record.getId());
        updateRecord.setRecordType(recordType1B.getName(), recordType1B.getVersion());

        Record updatedRecord = repository.update(updateRecord);

        // this should have done nothing
        assertEquals(record, repository.read(record.getId()));
    }

    @Test
    public void testUpdateWithNewRecordTypeVersionWithoutOtherChangesWithUseLatestRecordTypeFalse() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = repository.newRecord(record.getId());
        updateRecord.setRecordType(recordType1B.getName(), recordType1B.getVersion());

        // update with a version set and with useLatestRecordType=false, this should actually set the version to the
        // requested version (even though nothing else changed in the record)
        Record updatedRecord = repository.update(updateRecord, false, false);

        assertEquals(recordType1B.getName(), updatedRecord.getRecordTypeName());
        assertEquals(recordType1B.getVersion(), updatedRecord.getRecordTypeVersion());
        assertEquals(recordType1B.getName(), updatedRecord.getRecordTypeName(Scope.NON_VERSIONED));
        assertEquals(recordType1B.getVersion(), updatedRecord.getRecordTypeVersion(Scope.NON_VERSIONED));
        assertEquals(recordType1.getName(), updatedRecord.getRecordTypeName(Scope.VERSIONED));
        assertEquals(recordType1.getVersion(), updatedRecord.getRecordTypeVersion(Scope.VERSIONED));
        assertEquals(recordType1.getName(), updatedRecord.getRecordTypeName(Scope.VERSIONED_MUTABLE));
        assertEquals(recordType1.getVersion(), updatedRecord.getRecordTypeVersion(Scope.VERSIONED_MUTABLE));

        Record recordV1 = repository.read(record.getId(), Long.valueOf(1));
        assertEquals(recordType1B.getName(), recordV1.getRecordTypeName());
        assertEquals(recordType1B.getVersion(), recordV1.getRecordTypeVersion());
        assertEquals(recordType1B.getName(), recordV1.getRecordTypeName(Scope.NON_VERSIONED));
        assertEquals(recordType1B.getVersion(), recordV1.getRecordTypeVersion(Scope.NON_VERSIONED));
        assertEquals(recordType1.getName(), recordV1.getRecordTypeName(Scope.VERSIONED));
        assertEquals(recordType1.getVersion(), recordV1.getRecordTypeVersion(Scope.VERSIONED));
        assertEquals(recordType1.getName(), recordV1.getRecordTypeName(Scope.VERSIONED_MUTABLE));
        assertEquals(recordType1.getVersion(), recordV1.getRecordTypeVersion(Scope.VERSIONED_MUTABLE));
    }

    @Test
    public void testUpdateWithNewRecordTypeVersionOnlyOneFieldUpdated() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = repository.newRecord(record.getId());
        updateRecord.setRecordType(recordType1B.getName(), recordType1B.getVersion());
        updateRecord.setField(fieldType2.getName(), 789);

        // changed a versioned field, so versioned and non versioned scope should be on the new record type version
        Record updatedRecord = repository.update(updateRecord);
        assertEquals(recordType1B.getName(), updatedRecord.getRecordTypeName());
        assertEquals(recordType1B.getVersion(), updatedRecord.getRecordTypeVersion());
        assertEquals(recordType1B.getName(), updatedRecord.getRecordTypeName(Scope.VERSIONED));
        assertEquals(recordType1B.getVersion(), updatedRecord.getRecordTypeVersion(Scope.VERSIONED));
        assertEquals(recordType1B.getName(), updatedRecord.getRecordTypeName(Scope.NON_VERSIONED));
        assertEquals(recordType1B.getVersion(), updatedRecord.getRecordTypeVersion(Scope.NON_VERSIONED));

        // double check
        Record readRecord = repository.read(record.getId());
        assertEquals(recordType1B.getName(), updatedRecord.getRecordTypeName());
        assertEquals(recordType1B.getVersion(), updatedRecord.getRecordTypeVersion());
        assertEquals(recordType1B.getName(), readRecord.getRecordTypeName(Scope.NON_VERSIONED));
        assertEquals(recordType1B.getVersion(), readRecord.getRecordTypeVersion(Scope.NON_VERSIONED));
        assertEquals(recordType1B.getName(), updatedRecord.getRecordTypeName(Scope.VERSIONED));
        assertEquals(recordType1B.getVersion(), updatedRecord.getRecordTypeVersion(Scope.VERSIONED));
        assertEquals(recordType1.getName(), readRecord.getRecordTypeName(Scope.VERSIONED_MUTABLE));
        assertEquals(recordType1.getVersion(), readRecord.getRecordTypeVersion(Scope.VERSIONED_MUTABLE));
    }

    @Test
    public void testUpdateWithNewRecordType() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = repository.newRecord(record.getId());
        updateRecord.setRecordType(recordType2.getName(), recordType2.getVersion());
        updateRecord.setField(fieldType4.getName(), 1024);
        updateRecord.setField(fieldType5.getName(), false);
        updateRecord.setField(fieldType6.getName(), "value2");

        Record updatedRecord = repository.update(updateRecord);
        assertEquals(recordType2.getName(), updatedRecord.getRecordTypeName());
        assertEquals(recordType2.getVersion(), updatedRecord.getRecordTypeVersion());
        assertEquals(recordType2.getName(), updatedRecord.getRecordTypeName(Scope.NON_VERSIONED));
        assertEquals(recordType2.getVersion(), updatedRecord.getRecordTypeVersion(Scope.NON_VERSIONED));
        assertEquals(recordType2.getName(), updatedRecord.getRecordTypeName(Scope.VERSIONED));
        assertEquals(recordType2.getVersion(), updatedRecord.getRecordTypeVersion(Scope.VERSIONED));
        assertEquals(recordType2.getName(), updatedRecord.getRecordTypeName(Scope.VERSIONED_MUTABLE));
        assertEquals(recordType2.getVersion(), updatedRecord.getRecordTypeVersion(Scope.VERSIONED_MUTABLE));

        assertEquals(3, updatedRecord.getFields().size());

        Record readRecord = repository.read(record.getId());
        // Nothing got deleted
        assertEquals(6, readRecord.getFields().size());
        assertEquals("value1", readRecord.getField(fieldType1.getName()));
        assertEquals(1024, readRecord.getField(fieldType4.getName()));
        assertEquals(123, readRecord.getField(fieldType2.getName()));
        assertFalse((Boolean) readRecord.getField(fieldType5.getName()));
        assertTrue((Boolean) readRecord.getField(fieldType3.getName()));
        assertEquals("value2", readRecord.getField(fieldType6.getName()));
    }

    @Test
    public void testDeleteField() throws Exception {
        Record record = createDefaultRecord();
        Record deleteRecord = repository.newRecord(record.getId());
        deleteRecord.setRecordType(record.getRecordTypeName());
        deleteRecord.addFieldsToDelete(Arrays.asList(fieldType1.getName(), fieldType2.getName(), fieldType3.getName()));

        repository.update(deleteRecord);
        Record readRecord = repository.read(record.getId());
        assertEquals(0, readRecord.getFields().size());
    }

    @Test
    public void testDeleteFieldFollowedBySet() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName(), recordType1.getVersion());
        record.setField(fieldType1.getName(), "hello");
        record = repository.create(record);

        // Delete the field
        record.delete(fieldType1.getName(), true);
        record = repository.update(record);
        assertFalse(record.getFieldsToDelete().contains(fieldType1.getName()));

        // Set the field again
        record.setField(fieldType1.getName(), "hello");
        record = repository.update(record);
        assertEquals("hello", record.getField(fieldType1.getName()));

        // Check it also there after a fresh read
        record = repository.read(record.getId());
        assertEquals("hello", record.getField(fieldType1.getName()));

        // Calling delete field followed by set field should remove it from the deleted fields
        record.delete(fieldType1.getName(), true);
        assertTrue(record.getFieldsToDelete().contains(fieldType1.getName()));
        record.setField(fieldType1.getName(), "hello");
        assertFalse(record.getFieldsToDelete().contains(fieldType1.getName()));
    }

    @Test
    public void testDeleteFieldsNoLongerInRecordType() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = repository.newRecord(record.getId());
        updateRecord.setRecordType(recordType2.getName(), recordType2.getVersion());
        updateRecord.setField(fieldType4.getName(), 2222);
        updateRecord.setField(fieldType5.getName(), false);
        updateRecord.setField(fieldType6.getName(), "value2");

        repository.update(updateRecord);

        Record deleteRecord = repository.newRecord(record.getId());
        deleteRecord.setRecordType(recordType1.getName(), recordType1.getVersion());
        deleteRecord.addFieldsToDelete(Arrays.asList(fieldType1.getName()));
        repository.update(deleteRecord);

        Record readRecord = repository.read(record.getId());
        assertEquals(Long.valueOf(2), readRecord.getVersion());
        assertEquals(5, readRecord.getFields().size());
        try {
            if (avro) {
                System.out.println("Expecting FieldNotFoundException");
            }
            readRecord.getField(fieldType1.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }
        assertEquals("value2", readRecord.getField(fieldType6.getName()));
        assertEquals(2222, readRecord.getField(fieldType4.getName()));

        deleteRecord.addFieldsToDelete(Arrays.asList(fieldType2.getName(), fieldType3.getName()));
        repository.update(deleteRecord);

        readRecord = repository.read(record.getId());
        assertEquals(Long.valueOf(3), readRecord.getVersion());
        assertEquals(3, readRecord.getFields().size());
        assertEquals(2222, readRecord.getField(fieldType4.getName()));
        assertEquals(false, readRecord.getField(fieldType5.getName()));
        assertEquals("value2", readRecord.getField(fieldType6.getName()));
    }

    @Test
    public void testDeleteFieldTwice() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = repository.newRecord(record.getId());
        updateRecord.setRecordType(recordType2.getName(), recordType2.getVersion());
        updateRecord.setField(fieldType4.getName(), 2222);
        updateRecord.setField(fieldType5.getName(), false);
        updateRecord.setField(fieldType6.getName(), "value2");

        repository.update(updateRecord);

        Record deleteRecord = repository.newRecord(record.getId());
        deleteRecord.setRecordType(recordType1.getName(), recordType1.getVersion());
        deleteRecord.addFieldsToDelete(Arrays.asList(fieldType1.getName()));
        repository.update(deleteRecord);
        repository.update(deleteRecord);
    }

    @Test
    public void testUpdateAfterDelete() throws Exception {
        Record record = createDefaultRecord();
        Record deleteRecord = repository.newRecord(record.getId());
        deleteRecord.setRecordType(record.getRecordTypeName(), record.getRecordTypeVersion());
        deleteRecord.addFieldsToDelete(Arrays.asList(fieldType2.getName()));
        repository.update(deleteRecord);

        Record updateRecord = repository.newRecord(record.getId());
        updateRecord.setRecordType(record.getRecordTypeName(), record.getRecordTypeVersion());
        updateRecord.setField(fieldType2.getName(), 3333);
        repository.update(updateRecord);

        // Read version 3
        Record readRecord = repository.read(record.getId());
        assertEquals(Long.valueOf(3), readRecord.getVersion());
        assertEquals(3333, readRecord.getField(fieldType2.getName()));

        // Read version 2
        readRecord = repository.read(record.getId(), Long.valueOf(2));
        try {
            if (avro) {
                System.out.println("Expecting FieldNotFoundException");
            }
            readRecord.getField(fieldType2.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }

        // Read version 1
        readRecord = repository.read(record.getId(), Long.valueOf(1));
        assertEquals(123, readRecord.getField(fieldType2.getName()));
    }

    @Test
    public void testDeleteNonVersionableFieldAndUpdateVersionableField() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = repository.newRecord(record.getId());
        updateRecord.setRecordType(record.getRecordTypeName(), record.getRecordTypeVersion());
        updateRecord.setField(fieldType2.getName(), 999);
        updateRecord.addFieldsToDelete(Arrays.asList(fieldType1.getName()));
        repository.update(updateRecord);

        Record readRecord = repository.read(record.getId());
        assertEquals(999, readRecord.getField(fieldType2.getName()));
        try {
            if (avro) {
                System.out.println("Expecting FieldNotFoundException");
            }
            readRecord.getField(fieldType1.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }

        readRecord = repository.read(record.getId(), Long.valueOf(1));
        try {
            if (avro) {
                System.out.println("Expecting FieldNotFoundException");
            }
            readRecord.getField(fieldType1.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }

    }

    @Test
    public void testUpdateAndDeleteSameField() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = repository.newRecord(record.getId());
        updateRecord.setRecordType(record.getRecordTypeName(), record.getRecordTypeVersion());
        updateRecord.setField(fieldType2.getName(), 789);
        updateRecord.addFieldsToDelete(Arrays.asList(fieldType2.getName()));
        repository.update(updateRecord);

        try {
            if (avro) {
                System.out.println("Expecting FieldNotFoundException");
            }
            repository.read(record.getId()).getField(fieldType2.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }
    }

    @Test
    public void testDeleteRecordById() throws Exception {
        Record record = createDefaultRecord();
        repository.delete(record.getId());
        try {
            if (avro) {
                System.out.println("Expecting RecordNotFoundException");
            }
            repository.read(record.getId());
            fail();
        } catch (RecordNotFoundException expected) {
        }
        try {
            if (avro) {
                System.out.println("Expecting RecordNotFoundException");
            }
            repository.update(record);
            fail();
        } catch (RecordNotFoundException expected) {
        }
        try {
            if (avro) {
                System.out.println("Expecting RecordNotFoundException");
            }
            repository.delete(record.getId());
            fail();
        } catch (RecordNotFoundException expected) {
        }
    }


    @Test
    public void testDeleteRecord() throws Exception {
        Record record = createDefaultRecord();
        repository.delete(record);
        try {
            if (avro) {
                System.out.println("Expecting RecordNotFoundException");
            }
            repository.read(record.getId());
            fail();
        } catch (RecordNotFoundException expected) {
        }
        try {
            if (avro) {
                System.out.println("Expecting RecordNotFoundException");
            }
            repository.update(record);
            fail();
        } catch (RecordNotFoundException expected) {
        }
        try {
            if (avro) {
                System.out.println("Expecting RecordNotFoundException");
            }
            repository.delete(record.getId());
            fail();
        } catch (RecordNotFoundException expected) {
        }
    }

    @Test
    public void testDeleteRecordCleansUpData() throws Exception {
        Record record = createDefaultRecord();
        RecordId recordId = record.getId();
        repository.delete(recordId);

        record = repository.newRecord(recordId);
        record.setRecordType(recordType2.getName(), recordType2.getVersion());
        record.setField(fieldType4.getName(), 555);
        record.setField(fieldType5.getName(), false);
        record.setField(fieldType6.getName(), "zzz");
        repository.create(record);
        Record readRecord = repository.read(recordId);
        assertEquals(Long.valueOf(2), readRecord.getVersion());
        try {
            readRecord.getField(fieldType1.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }
        try {
            readRecord.getField(fieldType2.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }
        try {
            readRecord.getField(fieldType3.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }

        assertEquals(555, readRecord.getField(fieldType4.getName()));
        assertFalse((Boolean) readRecord.getField(fieldType5.getName()));
        assertEquals("zzz", readRecord.getField(fieldType6.getName()));
    }

    @Test
    public void testRecordRecreateFromVersionedToNonVersioned() throws Exception {
        QName vfield = new QName("recreate", "vfield");
        QName nvfield = new QName("recreate", "nvfield");

        FieldType vfieldtype = typeManager.newFieldType(typeManager.getValueType("STRING"), vfield, Scope.VERSIONED);
        vfieldtype = typeManager.createFieldType(vfieldtype);

        FieldType nvfieldtype =
                typeManager.newFieldType(typeManager.getValueType("STRING"), nvfield, Scope.NON_VERSIONED);
        nvfieldtype = typeManager.createFieldType(nvfieldtype);

        RecordType rt = typeManager.newRecordType(new QName("reinc", "rt"));
        rt.addFieldTypeEntry(vfieldtype.getId(), false);
        rt.addFieldTypeEntry(nvfieldtype.getId(), false);
        rt = typeManager.createRecordType(rt);

        // Create a record with versions
        RecordId recordId = repository.getIdGenerator().newRecordId();
        Record record = repository.newRecord(recordId);
        record.setRecordType(rt.getName());

        record.setField(vfield, "value 1");
        record = repository.createOrUpdate(record);

        record.setField(vfield, "value 2");
        record = repository.createOrUpdate(record);

        assertEquals(2L, record.getVersion().longValue());

        // Delete the record
        repository.delete(recordId);

        // Re-create the record, this time without versions
        record = repository.newRecord(recordId);
        record.setRecordType(rt.getName());
        record.setField(nvfield, "nv value 1");
        record = repository.createOrUpdate(record);

        assertEquals(null, record.getVersion());

        assertEquals(rt.getName(), record.getRecordTypeName());

        // Now add a version again, reusing last value from previously deleted record
        record.setField(vfield, "value 2");
        record = repository.createOrUpdate(record);

        assertEquals(3L, record.getVersion().longValue());
    }

    @Test
    public void testRecordRecreateOnlyVersionedFields() throws Exception {
        QName versionedOnlyQN = new QName(namespace, "VersionedOnly");
        RecordType versionedOnlyRT = typeManager.newRecordType(versionedOnlyQN);
        versionedOnlyRT.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType2.getId(), false));
        versionedOnlyRT = typeManager.createRecordType(versionedOnlyRT);

        Record record = repository.newRecord();
        record.setRecordType(versionedOnlyQN);
        record.setField(fieldType2.getName(), 111);
        record = repository.create(record);
        RecordId id = record.getId();
        repository.delete(id);

        record = repository.newRecord(id);
        record.setRecordType(versionedOnlyQN);
        record.setField(fieldType2.getName(), 222);
        record = repository.create(record);

        assertEquals(versionedOnlyQN, record.getRecordTypeName());

        record = repository.read(id);
        assertEquals(versionedOnlyQN, record.getRecordTypeName());
        assertEquals(versionedOnlyQN, record.getRecordTypeName(Scope.VERSIONED));
    }

    @Test
    public void testRecordRecreateNonVersionedOnly() throws Exception {
        QName nvfield = new QName("recreate", "OnlyNonVersioned");

        FieldType nvfieldtype =
                typeManager.newFieldType(typeManager.getValueType("STRING"), nvfield, Scope.NON_VERSIONED);
        nvfieldtype = typeManager.createFieldType(nvfieldtype);

        QName rtName = new QName("recreate", "rtOnlyNonVersioned");
        RecordType rt = typeManager.newRecordType(rtName);
        rt.addFieldTypeEntry(nvfieldtype.getId(), false);
        rt = typeManager.createRecordType(rt);

        // Create a record with versions
        RecordId recordId = repository.getIdGenerator().newRecordId();
        Record record = repository.newRecord(recordId);
        record.setRecordType(rt.getName());

        record.setField(nvfield, "nv value 1");
        record = repository.createOrUpdate(record);
        record = repository.read(record.getId());

        assertEquals("nv value 1", record.getField(nvfield));
        assertEquals(rtName, record.getRecordTypeName());

        assertEquals(null, record.getVersion());

        // Delete the record
        repository.delete(recordId);

        // Re-create the record,
        record = repository.newRecord(recordId);
        record.setRecordType(rt.getName());
        record.setField(nvfield, "nv value 2");
        record = repository.createOrUpdate(record);

        assertEquals(rtName, record.getRecordTypeName());
        assertEquals(null, record.getVersion());

        record = repository.read(record.getId());
        assertEquals("nv value 2", record.getField(nvfield));
        assertEquals(rtName, record.getRecordTypeName());
    }

    @Test
    public void testUpdateMutableField() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType2.getName(), recordType2.getVersion());
        record.setField(fieldType4.getName(), 123);
        record.setField(fieldType5.getName(), true);
        record.setField(fieldType6.getName(), "value1");
        record = repository.create(record);

        Record updateRecord = record.cloneRecord();
        updateRecord.setField(fieldType4.getName(), 456);
        updateRecord.setField(fieldType5.getName(), false);
        updateRecord.setField(fieldType6.getName(), "value2");
        repository.update(updateRecord);

        // Read version 1
        Record readRecord = repository.read(record.getId(), Long.valueOf(1));
        assertEquals(456, readRecord.getField(fieldType4.getName()));
        assertEquals(true, readRecord.getField(fieldType5.getName()));
        assertEquals("value1", readRecord.getField(fieldType6.getName()));

        // Update mutable version 1
        Record mutableRecord = repository.newRecord(record.getId());
        mutableRecord.setRecordType(recordType2.getName(), recordType2.getVersion());
        mutableRecord.setField(fieldType6.getName(), "value3");
        mutableRecord.setVersion(1L);
        mutableRecord = repository.update(mutableRecord, true, false);

        // Read version 1 again
        readRecord = repository.read(record.getId(), 1L);
        assertEquals(456, readRecord.getField(fieldType4.getName()));
        assertEquals(true, readRecord.getField(fieldType5.getName()));
        assertEquals("value3", readRecord.getField(fieldType6.getName()));

        // Update mutable version 2
        mutableRecord.setVersion(2L);
        mutableRecord.setField(fieldType6.getName(), "value4");
        mutableRecord = repository.update(mutableRecord, true, false);

        // Read version 2
        readRecord = repository.read(record.getId(), 2L);
        assertEquals(456, readRecord.getField(fieldType4.getName()));
        assertEquals(false, readRecord.getField(fieldType5.getName()));
        assertEquals("value4", readRecord.getField(fieldType6.getName()));
    }

    @Test
    public void testUpdateMutableFieldWithNewRecordType() throws Exception {
        // Create default record
        Record record = createDefaultRecord();

        // Update the record, creates a second version
        Record updateRecord = record.cloneRecord();
        updateRecord.setField(fieldType1.getName(), "value2");
        updateRecord.setField(fieldType2.getName(), 789);
        updateRecord.setField(fieldType3.getName(), false);
        repository.update(updateRecord, false, false);

        // Read the first version of the record
        Record readRecord = repository.read(record.getId(), Long.valueOf(1));
        assertEquals(recordType1.getName(), readRecord.getRecordTypeName());
        assertEquals(recordType1.getVersion(), readRecord.getRecordTypeVersion());
        assertEquals(recordType1.getName(), readRecord.getRecordTypeName(Scope.NON_VERSIONED));
        assertEquals(recordType1.getVersion(), readRecord.getRecordTypeVersion(Scope.NON_VERSIONED));
        assertEquals(recordType1.getName(), readRecord.getRecordTypeName(Scope.VERSIONED));
        assertEquals(recordType1.getVersion(), readRecord.getRecordTypeVersion(Scope.VERSIONED));
        assertEquals(recordType1.getName(), readRecord.getRecordTypeName(Scope.VERSIONED_MUTABLE));
        assertEquals(recordType1.getVersion(), readRecord.getRecordTypeVersion(Scope.VERSIONED_MUTABLE));

        // Do a mutable update of the first version of the record, change the record type
        Record updateMutableRecord = repository.newRecord(record.getId());
        updateMutableRecord.setVersion(Long.valueOf(1));
        updateMutableRecord.setRecordType(recordType2.getName(), recordType2.getVersion());
        updateMutableRecord.setField(fieldType4.getName(), 888);
        updateMutableRecord.setField(fieldType5.getName(), false);
        updateMutableRecord.setField(fieldType6.getName(), "value3");

        Record updatedMutableRecord = repository.update(updateMutableRecord, true, false);
        assertEquals(Long.valueOf(1), updatedMutableRecord.getVersion());

        // Read the first version of the record again
        readRecord = repository.read(record.getId(), Long.valueOf(1));
        assertEquals(Long.valueOf(1), readRecord.getVersion());
        assertEquals("value2", readRecord.getField(fieldType1.getName()));
        assertEquals(123, readRecord.getField(fieldType2.getName()));
        assertEquals(true, readRecord.getField(fieldType3.getName()));
        // Only the mutable fields got updated
        assertEquals("value3", readRecord.getField(fieldType6.getName()));
        try {
            if (avro) {
                System.out.println("Expecting FieldNotFoundException");
            }
            readRecord.getField(fieldType4.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }
        try {
            if (avro) {
                System.out.println("Expecting FieldNotFoundException");
            }
            readRecord.getField(fieldType5.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }
        assertEquals(recordType1.getName(), readRecord.getRecordTypeName());
        assertEquals(recordType1.getVersion(), readRecord.getRecordTypeVersion());
        assertEquals(recordType1.getName(), readRecord.getRecordTypeName(Scope.NON_VERSIONED));
        assertEquals(recordType1.getVersion(), readRecord.getRecordTypeVersion(Scope.NON_VERSIONED));
        assertEquals(recordType1.getName(), readRecord.getRecordTypeName(Scope.VERSIONED));
        assertEquals(recordType1.getVersion(), readRecord.getRecordTypeVersion(Scope.VERSIONED));
        // The mutable record type should have been changed
        assertEquals(recordType2.getName(), readRecord.getRecordTypeName(Scope.VERSIONED_MUTABLE));
        assertEquals(recordType2.getVersion(), readRecord.getRecordTypeVersion(Scope.VERSIONED_MUTABLE));

        // Read the second version again of the record
        readRecord = repository.read(record.getId());
        assertEquals(Long.valueOf(2), readRecord.getVersion());
        assertEquals("value2", readRecord.getField(fieldType1.getName()));
        assertEquals(789, readRecord.getField(fieldType2.getName()));
        assertEquals(false, readRecord.getField(fieldType3.getName()));
        assertEquals("value3", readRecord.getField(fieldType6.getName()));
        assertEquals(recordType1.getName(), readRecord.getRecordTypeName());
        assertEquals(recordType1.getName(), readRecord.getRecordTypeName(Scope.NON_VERSIONED));
        assertEquals(recordType1.getName(), readRecord.getRecordTypeName(Scope.VERSIONED));
        // The original mutable record type should have been copied to the next version
        assertEquals(recordType1.getName(), readRecord.getRecordTypeName(Scope.VERSIONED_MUTABLE));
    }

    @Test
    public void testUpdateMutableFieldCopiesValueToNext() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = record.cloneRecord();
        updateRecord.setField(fieldType1.getName(), "value2");
        updateRecord.setField(fieldType2.getName(), 789);
        updateRecord = repository.update(updateRecord); // Leave mutable field
        // same on first update

        updateRecord.setField(fieldType3.getName(), false);
        updateRecord = repository.update(updateRecord);

        Record readRecord = repository.read(record.getId(), Long.valueOf(2));
        assertEquals(true, readRecord.getField(fieldType3.getName()));

        updateRecord = repository.newRecord(record.getId());
        updateRecord.setRecordType(recordType1.getName(), recordType1.getVersion());
        updateRecord.setField(fieldType3.getName(), false);
        updateRecord.setVersion(1L);

        repository.update(updateRecord, true, false);

        readRecord = repository.read(record.getId(), Long.valueOf(1));
        assertFalse((Boolean) readRecord.getField(fieldType3.getName()));
        readRecord = repository.read(record.getId(), Long.valueOf(2));
        assertTrue((Boolean) readRecord.getField(fieldType3.getName()));
        readRecord = repository.read(record.getId(), Long.valueOf(3));
        assertFalse((Boolean) readRecord.getField(fieldType3.getName()));
    }

    @Test
    public void testDeleteMutableField() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = record.cloneRecord();
        updateRecord.setField(fieldType1.getName(), "value2");
        updateRecord.setField(fieldType2.getName(), 789);
        updateRecord.setField(fieldType3.getName(), false);
        repository.update(updateRecord);

        Record deleteRecord = repository.newRecord(record.getId());
        deleteRecord.setVersion(Long.valueOf(1));
        deleteRecord.setRecordType(record.getRecordTypeName(), record.getRecordTypeVersion());
        deleteRecord.addFieldsToDelete(Arrays.asList(fieldType1.getName(), fieldType2.getName(), fieldType3.getName()));

        repository.update(deleteRecord, true, false);

        Record readRecord = repository.read(record.getId(), Long.valueOf(1));
        // The non-mutable fields were ignored
        assertEquals("value2", readRecord.getField(fieldType1.getName()));
        assertEquals(123, readRecord.getField(fieldType2.getName()));
        try {
            // The mutable field got deleted
            if (avro) {
                System.out.println("Expecting FieldNotFoundException");
            }
            readRecord.getField(fieldType3.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }

        readRecord = repository.read(record.getId());
        assertEquals(false, readRecord.getField(fieldType3.getName()));
    }

    @Test
    public void testDeleteMutableFieldCopiesValueToNext() throws Exception {
        Record record = createDefaultRecord();
        Record updateRecord = record.cloneRecord();
        updateRecord.setField(fieldType1.getName(), "value2");
        updateRecord.setField(fieldType2.getName(), 789);
        updateRecord = repository.update(updateRecord); // Leave mutable field
        // same on first update

        updateRecord.setField(fieldType3.getName(), false);
        updateRecord = repository.update(updateRecord);

        Record readRecord = repository.read(record.getId(), Long.valueOf(2));
        assertEquals(true, readRecord.getField(fieldType3.getName()));

        Record deleteMutableFieldRecord = repository.newRecord(record.getId());
        deleteMutableFieldRecord.setVersion(Long.valueOf(1));
        deleteMutableFieldRecord.setRecordType(record.getRecordTypeName(), record.getRecordTypeVersion());
        deleteMutableFieldRecord.addFieldsToDelete(Arrays.asList(fieldType3.getName()));

        repository.update(deleteMutableFieldRecord, true, false);

        readRecord = repository.read(record.getId(), Long.valueOf(1));
        try {
            if (avro) {
                System.out.println("Expecting FieldNotFoundException");
            }
            readRecord.getField(fieldType3.getName());
            fail();
        } catch (FieldNotFoundException expected) {
        }

        readRecord = repository.read(record.getId(), Long.valueOf(2));
        assertEquals(true, readRecord.getField(fieldType3.getName()));

        readRecord = repository.read(record.getId());
        assertEquals(false, readRecord.getField(fieldType3.getName()));
    }

    @Test
    public void testSupertypeLatestVersion() throws Exception {
        RecordType recordType4 = typeManager.newRecordType(new QName(namespace, "RT4"));
        recordType4.addFieldTypeEntry(typeManager.newFieldTypeEntry(fieldType6.getId(), false));
        recordType4.addSupertype(recordType1.getId()); // In fact recordType1B should be taken as supertype
        recordType4 = typeManager.createRecordType(recordType4);

        Record record = repository.newRecord(idGenerator.newRecordId());
        record.setRecordType(recordType4.getName(), recordType4.getVersion());
        record.setField(fieldType1.getName(), "foo");
        record.setField(fieldType2.getName(), 555);
        record.setField(fieldType3.getName(), true);
        record.setField(fieldType1B.getName(), "fromLatestSupertypeRecordTypeVersion");
        record.setField(fieldType6.getName(), "bar");
        record = repository.create(record);

        Record readRecord = repository.read(record.getId());
        assertEquals(Long.valueOf(1), readRecord.getVersion());
        assertEquals("foo", readRecord.getField(fieldType1.getName()));
        assertEquals(555, readRecord.getField(fieldType2.getName()));
        assertEquals(true, readRecord.getField(fieldType3.getName()));
        assertEquals("fromLatestSupertypeRecordTypeVersion", readRecord.getField(fieldType1B.getName()));
        assertEquals("bar", readRecord.getField(fieldType6.getName()));
    }

    @Test
    public void testNonVersionedToVersioned() throws Exception {
        // Create a record with only a versioned and non-versioned field
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName(), recordType1.getVersion());
        record.setField(fieldType1.getName(), "hello");
        record.setField(fieldType2.getName(), new Integer(4));
        record = repository.create(record);

        // Try to read the created version
        record = repository.read(record.getId(), 1L);
    }

    @Test
    public void testIdRecord() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName(), recordType1.getVersion());
        record.setField(fieldType1.getName(), "hello");
        record.setField(fieldType2.getName(), new Integer(4));
        record = repository.create(record);

        IdRecord idRecord = repository.readWithIds(record.getId(), null, null);
        assertEquals("hello", idRecord.getField(fieldType1.getId()));
        assertTrue(idRecord.hasField(fieldType1.getId()));
        assertEquals(new Integer(4), idRecord.getField(fieldType2.getId()));
        assertTrue(idRecord.hasField(fieldType2.getId()));

        Map<SchemaId, Object> fields = idRecord.getFieldsById();
        assertEquals(2, fields.size());
        assertEquals("hello", fields.get(fieldType1.getId()));
        assertEquals(new Integer(4), fields.get(fieldType2.getId()));

        assertEquals(record, idRecord.getRecord());
    }

    @Test
    public void testVersionNumbers() throws Exception {
        // Create a record without versioned fields, the record will be without versions
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName(), recordType1.getVersion());
        record.setField(fieldType1.getName(), "hello");
        record = repository.create(record);

        // Check the version is null
        assertEquals(null, record.getVersion());

        // Check version number stays null after additional update
        record.setField(fieldType1.getName(), "hello2");
        repository.update(record);
        record = repository.read(record.getId());
        assertEquals(null, record.getVersion());

        // add a versioned field to the record
        record.setField(fieldType2.getName(), new Integer(4));
        record = repository.update(record);
        assertEquals(new Long(1), record.getVersion());

        // Verify the last version number after a fresh record read
        record = repository.read(record.getId());
        assertEquals(new Long(1), record.getVersion());

        // Read specific version
        record = repository.read(record.getId(), 1L);
        assertEquals(new Long(1), record.getVersion());
        assertTrue(record.hasField(fieldType2.getName()));
        assertEquals(2, record.getFields().size());

        try {
            if (avro) {
                System.out.println("Expecting VersionNotFoundException");
            }
            record = repository.read(record.getId(), 2L);
            fail("expected exception");
        } catch (VersionNotFoundException e) {
            // expected
        }
    }

    @Test
    public void testValidateCreate() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType3.getName(), 1L);
        record.setField(fieldType2.getName(), 123);
        repository.create(record);

        record = repository.newRecord();
        record.setRecordType(recordType3.getName(), 2L);
        record.setField(fieldType2.getName(), 123);
        try {
            if (avro) {
                System.out.println("Expecting InvalidRecordException");
            }
            repository.create(record);
            fail();
        } catch (InvalidRecordException expected) {
        }

        record = repository.newRecord();
        record.setRecordType(recordType3.getName(), 2L);
        record.setField(fieldType1.getName(), "abc");
        record.setField(fieldType2.getName(), 123);
    }

    @Test
    public void testValidateUpdate() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType3.getName(), 1L);
        record.setField(fieldType2.getName(), 123);
        record = repository.create(record);

        record.setRecordType(recordType3.getName(), 2L);
        record.setField(fieldType2.getName(), 567);
        try {
            if (avro) {
                System.out.println("Expecting InvalidRecordException");
            }
            repository.update(record, false, false);
            fail();
        } catch (InvalidRecordException expected) {
        }

        record.setField(fieldType1.getName(), "abc");
        repository.update(record, false, false);
    }

    @Test
    public void testValidateMutableUpdate() throws Exception {
        // Nothing mandatory
        Record record = repository.newRecord();
        record.setRecordType(recordType3.getName(), 1L);
        record.setField(fieldType2.getName(), 123);
        record = repository.create(record);

        // Non-versioned field1 mandatory
        record = repository.newRecord(record.getId());
        record.setRecordType(recordType3.getName(), 2L);
        record.setField(fieldType1.getName(), "abc");
        repository.update(record, false, false); // record version 1

        // Mutable field3 mandatory
        record.setRecordType(recordType3.getName(), 3L);
        record.setField(fieldType1.getName(), "efg");
        try {
            if (avro) {
                System.out.println("Expecting InvalidRecordException");
            }
            repository.update(record, false, false);
            fail();
        } catch (InvalidRecordException expected) {
        }

        // Mutable field3 not mandatory
        record = repository.newRecord(record.getId());
        record.setRecordType(recordType3.getName(), 2L);
        record.setField(fieldType3.getName(), true);
        repository.update(record, false, false); // record version 2

        // Mutable field update of record version 1 with field3 mandatory
        // Field3 already exists, but in record version 2 not version 1
        record = repository.newRecord(record.getId());
        record.setRecordType(recordType3.getName(), 4L);
        record.setField(fieldType6.getName(), "zzz");
        record.setVersion(1L);
        try {
            if (avro) {
                System.out.println("Expecting InvalidRecordException");
            }
            repository.update(record, true, false);
            fail();
        } catch (InvalidRecordException expected) {
        }
        record.setField(fieldType3.getName(), false);
        repository.update(record, true, false);
    }

    @Test
    public void testCreateOrUpdate() throws Exception {
        RecordId id = idGenerator.newRecordId();
        Record record = repository.newRecord(id);
        record.setRecordType(recordType1.getName(), recordType1.getVersion());
        record.setField(fieldType1.getName(), "value1");

        Record resultRecord;
        resultRecord = repository.createOrUpdate(record);
        assertEquals(ResponseStatus.CREATED, resultRecord.getResponseStatus());
        resultRecord = repository.createOrUpdate(record);
        assertEquals(ResponseStatus.UP_TO_DATE, resultRecord.getResponseStatus());

        record.setField(fieldType1.getName(), "value2");
        resultRecord = repository.createOrUpdate(record);
        assertEquals(ResponseStatus.UPDATED, resultRecord.getResponseStatus());
        resultRecord = repository.createOrUpdate(record);
        assertEquals(ResponseStatus.UP_TO_DATE, resultRecord.getResponseStatus());
        resultRecord = repository.createOrUpdate(record);
        assertEquals(ResponseStatus.UP_TO_DATE, resultRecord.getResponseStatus());
    }

    @Test
    public void testUpdateMutableFieldsRecordType() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType2.getName(), recordType2.getVersion());
        record.setField(fieldType4.getName(), 123);
        record.setField(fieldType5.getName(), true);
        record.setField(fieldType6.getName(), "value1");
        record = repository.create(record);

        // Updating versioned mutable fields should not require record type to be specified in the record
        record = repository.newRecord(record.getId());
        record.setVersion(1L);
        record.setField(fieldType6.getName(), "value2");
        record = repository.update(record, true, true);

        // Change record to a different record type
        RecordType recordTypeA = typeManager.newRecordType(new QName("testmut", "RTA"));
        recordTypeA.addFieldTypeEntry(fieldType4.getId(), false);
        recordTypeA.addFieldTypeEntry(fieldType5.getId(), false);
        recordTypeA.addFieldTypeEntry(fieldType6.getId(), false);
        recordTypeA = typeManager.createRecordType(recordTypeA);

        // Change the record type of the non-versioned scope (at the time of this writing, could not modify
        // just the record type of a record, hence also touching a field)
        record = repository.read(record.getId());
        record.setRecordType(recordTypeA.getName(), null);
        record.setField(fieldType4.getName(), 456);
        record = repository.update(record);
        record = repository.read(record.getId());
        assertEquals(recordTypeA.getName(), record.getRecordTypeName());

        // The record type of the versioned mutable scope should still be what it was before
        assertEquals(recordType2.getName(), record.getRecordTypeName(Scope.VERSIONED_MUTABLE));

        // Now update a versioned-mutable field without specifying a record type, would expect it to move
        // also to the new record type of the non-versioned scope.
        record = repository.newRecord(record.getId());
        record.setVersion(1L);
        record.setField(fieldType6.getName(), "value3");
        record = repository.update(record, true, true);
        assertEquals(recordTypeA.getName(), record.getRecordTypeName(Scope.VERSIONED_MUTABLE));
        assertEquals(recordTypeA.getVersion(), record.getRecordTypeVersion(Scope.VERSIONED_MUTABLE));
    }

    @Test
    public void testReadMultipleRecords() throws Exception {
        Record record1 = createDefaultRecord();
        Record record2 = createDefaultRecord();
        Record record3 = createDefaultRecord();

        List<Record> readRecords = repository.read(Arrays.asList(record3.getId(), record1.getId()));

        assertEquals(2, readRecords.size());
        assertTrue(readRecords.contains(record1));
        assertTrue(readRecords.contains(record3));

        repository.delete(record2.getId());
        readRecords = repository.read(Arrays.asList(record2.getId(), record1.getId()));
        assertEquals(1, readRecords.size());
        assertTrue(readRecords.contains(record1));

        readRecords = repository.read(Collections.<RecordId>emptyList());
        assertTrue(readRecords.isEmpty());
    }

    @Test
    public void testConditionalUpdate() throws Exception {
        Record record = createDefaultRecord();

        //
        // Single condition
        //
        record.setField(fieldType1.getName(), "value2");
        record = repository
                .update(record, Collections.singletonList(new MutationCondition(fieldType1.getName(), "xyz")));

        assertEquals(ResponseStatus.CONFLICT, record.getResponseStatus());
        assertEquals("value1", record.getField(fieldType1.getName()));

        // Check repository state was really not modified
        record = repository.read(record.getId());
        assertEquals("value1", record.getField(fieldType1.getName()));

        //
        // Multiple conditions
        //
        List<MutationCondition> conditions = new ArrayList<MutationCondition>();
        conditions.add(new MutationCondition(fieldType1.getName(), "value1")); // evals to true
        conditions.add(new MutationCondition(fieldType2.getName(), 123)); // evals to true
        conditions.add(new MutationCondition(fieldType3.getName(), false)); // evals to false

        record.setField(fieldType1.getName(), "value2");
        record = repository.update(record, conditions);

        assertEquals(ResponseStatus.CONFLICT, record.getResponseStatus());
        assertEquals("value1", record.getField(fieldType1.getName()));

        // Check repository state was really not modified
        record = repository.read(record.getId());
        assertEquals("value1", record.getField(fieldType1.getName()));

        // reset record state
        record = createDefaultRecord();

        //
        // Not-equals condition
        //
        record.setField(fieldType1.getName(), "value2");
        record = repository.update(record,
                Collections.singletonList(new MutationCondition(fieldType1.getName(), CompareOp.NOT_EQUAL, "value1")));

        assertEquals(ResponseStatus.CONFLICT, record.getResponseStatus());
        assertEquals("value1", record.getField(fieldType1.getName()));

        //
        // Other than equals conditions
        //
        for (CompareOp op : CompareOp.values()) {
            List<Integer> testValues = new ArrayList<Integer>();
            switch (op) {
                case LESS:
                    testValues.add(123);
                    testValues.add(122);
                    break;
                case LESS_OR_EQUAL:
                    testValues.add(122);
                    break;
                case EQUAL:
                    testValues.add(122);
                    testValues.add(124);
                    break;
                case NOT_EQUAL:
                    testValues.add(123);
                    break;
                case GREATER_OR_EQUAL:
                    testValues.add(124);
                    break;
                case GREATER:
                    testValues.add(123);
                    testValues.add(124);
            }

            for (Integer testValue : testValues) {
                record.setField(fieldType2.getName(), 999);
                record = repository.update(record,
                        Collections.singletonList(new MutationCondition(fieldType2.getName(), op, testValue)));

                assertEquals(ResponseStatus.CONFLICT, record.getResponseStatus());
                assertEquals(123, record.getField(fieldType2.getName()));
            }
        }

        //
        // Allow missing fields
        //

        record.setField(fieldType1.getName(), "value2");
        // note that we're testing on field 1B!
        record = repository.update(record,
                Collections.singletonList(
                        new MutationCondition(fieldType1B.getName(), CompareOp.EQUAL, "whatever", true)));

        assertEquals(ResponseStatus.UPDATED, record.getResponseStatus());
        assertEquals("value2", record.getField(fieldType1.getName()));

        // reset record state
        record.setField(fieldType1.getName(), "value1");
        record = repository.update(record);

        //
        // Test for missing/present field
        //

        // Field MUST be missing
        record.setField(fieldType1.getName(), "value2");
        record = repository.update(record,
                Collections.singletonList(new MutationCondition(fieldType1.getName(), CompareOp.EQUAL, null)));

        assertEquals(ResponseStatus.CONFLICT, record.getResponseStatus());
        assertEquals("value1", record.getField(fieldType1.getName()));

        // Field MUST NOT be missing (but can have whatever value) -- note that we test on field 1B!
        record.setField(fieldType1.getName(), "value2");
        record = repository.update(record,
                Collections.singletonList(new MutationCondition(fieldType1B.getName(), CompareOp.NOT_EQUAL, null)));

        assertEquals(ResponseStatus.CONFLICT, record.getResponseStatus());
        assertEquals("value1", record.getField(fieldType1.getName()));

        // Same, successful case
        record.setField(fieldType1.getName(), "value2");
        record = repository.update(record,
                Collections.singletonList(new MutationCondition(fieldType1.getName(), CompareOp.NOT_EQUAL, null)));

        assertEquals(ResponseStatus.UPDATED, record.getResponseStatus());
        assertEquals("value2", record.getField(fieldType1.getName()));

        // reset record state
        record.setField(fieldType1.getName(), "value1");
        record = repository.update(record);

        //
        // Supplied values differ from field type (classcastexception)
        //

        // TODO
//        record.setField(fieldType1.getName(), "value2");
//        try {
//            repository.update(record, Collections.singletonList(new MutationCondition(fieldType1.getName(), new Long(55))));
//            fail("Expected an exception");
//        } catch (ClassCastException e) {
//            // expected
//        }

        //
        // Test on system fields
        //

        final QName versionField = new QName("org.lilyproject.system", "version");

        // Create new record to be sure numbering starts from 1
        record = createDefaultRecord();

        record.setField(fieldType2.getName(), new Integer(55));
        record = repository.update(record,
                Collections.singletonList(new MutationCondition(versionField, CompareOp.EQUAL, new Long(2))));
        assertEquals(ResponseStatus.CONFLICT, record.getResponseStatus());

        record.setField(fieldType2.getName(), new Integer(55));
        record = repository.update(record,
                Collections.singletonList(new MutationCondition(versionField, CompareOp.EQUAL, new Long(1))));
        assertEquals(ResponseStatus.UPDATED, record.getResponseStatus());
        assertEquals(new Long(2), record.getVersion());
        assertEquals(new Integer(55), record.getField(fieldType2.getName()));

        // Test behavior in case of null version
        record = repository.newRecord();
        record.setRecordType(recordType1.getName(), recordType1.getVersion());
        record.setField(fieldType1.getName(), "value1");
        record = repository.create(record);

        record.setField(fieldType1.getName(), "value2");
        record = repository.update(record,
                Collections.singletonList(new MutationCondition(versionField, CompareOp.EQUAL, new Long(1))));
        assertEquals(ResponseStatus.CONFLICT, record.getResponseStatus());

        record.setField(fieldType1.getName(), "value2");
        record = repository.update(record,
                Collections.singletonList(new MutationCondition(versionField, CompareOp.EQUAL, null)));
        assertEquals(ResponseStatus.UPDATED, record.getResponseStatus());

        //
        // Test conditional update on update of version-mutable fields
        //
        record = createDefaultRecord();

        record.setField(fieldType3.getName(), false);
        record = repository.update(record, true, true,
                Collections.singletonList(new MutationCondition(fieldType3.getName(), CompareOp.EQUAL, Boolean.FALSE)));
        assertEquals(ResponseStatus.CONFLICT, record.getResponseStatus());

        record.setField(fieldType3.getName(), false);
        record = repository.update(record, true, true,
                Collections.singletonList(new MutationCondition(fieldType3.getName(), CompareOp.EQUAL, Boolean.TRUE)));
        assertEquals(ResponseStatus.UPDATED, record.getResponseStatus());

        // In case of versioned-mutable update, we can also add conditions on versioned and non-versioned fields
        conditions = new ArrayList<MutationCondition>();
        conditions.add(new MutationCondition(fieldType1.getName(), "value1")); // evals to true
        conditions.add(new MutationCondition(fieldType2.getName(), 124)); // evals to true

        record.setField(fieldType3.getName(), true);
        record = repository.update(record, true, true, conditions);
        assertEquals(ResponseStatus.CONFLICT, record.getResponseStatus());
    }

    @Test
    public void testConditionUpdateConcurrency() throws Exception {
        // This test uses MutationCondition's to let multiple threads concurrently increment a counter field
        // in the same record.

        // Create record with counter field initially set to 0
        Record record = repository.newRecord();
        record.setId(repository.getIdGenerator().newRecordId());
        record.setRecordType(recordType2.getName());
        record.setField(fieldType4.getName(), new Integer(0));
        repository.createOrUpdate(record);
        final RecordId recordId = record.getId();

        // Run concurrent threads to increment the counter field
        int threads = 5;
        int count = 200;
        ThreadPoolExecutor executor = new ThreadPoolExecutor(threads, threads, 10, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(count));

        List<Future> futures = new ArrayList<Future>();
        for (int i = 0; i < count; i++) {
            futures.add(executor.submit(new Callable<Void>() {
                @Override
                public Void call() throws Exception {
                    int iteration = 0;
                    while (true) {
                        try {
                            iteration++;
                            Record record = repository.read(recordId);
                            int oldValue = (Integer) record.getField(fieldType4.getName());
                            record.setField(fieldType4.getName(), new Integer(oldValue + 1));
                            MutationCondition cond = new MutationCondition(fieldType4.getName(), oldValue, false);
                            record = repository.update(record, Lists.newArrayList(cond));

                            if (record.getResponseStatus() == ResponseStatus.CONFLICT) {
                                if (iteration > 20) {
                                    System.out.println("cas failed, will retry, iteration " + iteration);
                                }
                                Thread.sleep((int) (Math.random() * 50));
                            } else if (record.getResponseStatus() == ResponseStatus.UPDATED) {
                                // success
                                return null;
                            } else {
                                fail("unexpected response status = " + record.getResponseStatus());
                            }
                        } catch (ConcurrentRecordUpdateException e) {
                            if (iteration > 20) {
                                System.out.println("concurrent update, will retry, iteration " + iteration);
                            }
                            Thread.sleep((int) (Math.random() * 50));
                        }
                    }
                }
            }));
        }

        for (Future future : futures) {
            future.get();
        }

        executor.shutdown();

        // verify the value of the counter field is as expected
        record = repository.read(record.getId());
        assertEquals(count, record.getField(fieldType4.getName()));
    }

    @Test
    public void testConditionalDelete() throws Exception {
        Record record = createDefaultRecord();

        // perform delete with not-satisfied conditions
        record = repository.delete(record.getId(),
                Collections.singletonList(new MutationCondition(fieldType1.getName(), "xyz")));

        assertNotNull(record);
        assertEquals(ResponseStatus.CONFLICT, record.getResponseStatus());
        assertEquals("value1", record.getField(fieldType1.getName()));
        assertEquals(1, record.getFields().size());

        // check record was surely not deleted
        record = repository.read(record.getId());

        // perform delete with satisfied conditions
        record = repository.delete(record.getId(),
                Collections.singletonList(new MutationCondition(fieldType1.getName(), "value1")));
        assertNull(record);
    }

    @Test
    public void testRecordWithLinkFields() throws Exception {
        FieldType linkFieldType = typeManager.createFieldType(typeManager
                .newFieldType(typeManager.getValueType("LINK"), new QName("testRecordWithLinkFields", "linkFieldType"),
                        Scope.NON_VERSIONED));

        RecordType recordTypeWithLink = typeManager.newRecordType(new QName(namespace, "recordTypeWithLink"));
        recordTypeWithLink.addFieldTypeEntry(typeManager.newFieldTypeEntry(linkFieldType.getId(), false));
        recordTypeWithLink = typeManager.createRecordType(recordTypeWithLink);

        // Create records to link to
        Record record = createDefaultRecord();
        Record record2 = createDefaultRecord();

        // Create record with link to record
        Record recordWithLinks = repository.newRecord();
        recordWithLinks.setRecordType(recordTypeWithLink.getName());
        Link link = Link.newBuilder().recordId(record.getId()).copyAll(false).create();
        recordWithLinks.setField(linkFieldType.getName(), link);
        recordWithLinks = repository.create(recordWithLinks);

        // Validate link is created
        link = (Link) recordWithLinks.getField(linkFieldType.getName());
        assertEquals(record.getId(), link.getMasterRecordId());

        // Read record again and validate link is there
        recordWithLinks = repository.read(recordWithLinks.getId());
        link = (Link) recordWithLinks.getField(linkFieldType.getName());
        assertEquals(record.getId(), link.getMasterRecordId());

        // Update record with link to record2
        recordWithLinks = repository.newRecord(recordWithLinks.getId());
        link = Link.newBuilder().recordId(record2.getId()).copyAll(false).create();
        recordWithLinks.setField(linkFieldType.getName(), link);
        recordWithLinks = repository.update(recordWithLinks);

        // Validate link is updated
        link = (Link) recordWithLinks.getField(linkFieldType.getName());
        assertEquals(record2.getId(), link.getMasterRecordId());

        // Read record and validate link is still updated
        recordWithLinks = repository.read(recordWithLinks.getId());
        link = (Link) recordWithLinks.getField(linkFieldType.getName());
        assertEquals(record2.getId(), link.getMasterRecordId());
    }

    @Test
    public void testRecordBuilder() throws Exception {
        RecordBuilder builder = repository.recordBuilder();
        Record record = builder.recordType(recordType1.getName())
                .field(fieldType1.getName(), "abc")
                .field(fieldType2.getName(), 123)
                .field(fieldType3.getName(), true)
                .create();
        assertEquals(record, repository.read(record.getId()));

        builder.reset();
        Record record2 = builder.recordType(recordType2.getName())
                .field(fieldType4.getName(), 999)
                .field(fieldType5.getName(), true)
                .field(fieldType6.getName(), "xyz")
                .create();

        Record readRecord = repository.read(record2.getId());
        assertEquals(999, readRecord.getField(fieldType4.getName()));
        try {
            readRecord.getField(fieldType1.getName());
            fail("FieldType1 not expected. Builder should have been reset");
        } catch (FieldNotFoundException expected) {
        }
    }

    @Test
    public void testDefaultNamespace() throws Exception {
        RecordBuilder builder = repository.recordBuilder();
        Record record = builder.defaultNamespace(namespace)
                .recordType("RT1")
                .field("field1", "abc")
                .field("field2", 123)
                .field("field3", true)
                .create();
        Record readRecord = repository.read(record.getId());
        assertEquals(record, readRecord);

        assertEquals("abc", readRecord.getField("field1"));
        readRecord.setDefaultNamespace("anotherNamespace");
        try {
            readRecord.getField("field1");
        } catch (FieldNotFoundException expected) {
        }
    }

    @Test
    public void testRecordBuilderCreateOrUpdate() throws Exception {
        try {
            repository
                    .recordBuilder()
                    .defaultNamespace(namespace)
                    .recordType("RT1")
                    .field("field1", "abc")
                    .createOrUpdate();
            fail("expected exception");
        } catch (RecordException e) {
            // expected
        }

        Record record = repository
                .recordBuilder()
                .assignNewUuid()
                .defaultNamespace(namespace)
                .recordType("RT1")
                .field("field1", "abc")
                .createOrUpdate();

        repository
                .recordBuilder()
                .id(record.getId())
                .defaultNamespace(namespace)
                .field("field1", "def")
                .createOrUpdate();
    }

    @Test
    public void testRecordBuilderNestedRecords() throws Exception {
        String NS = "testRecordBuilderNestedRecords";

        typeManager
                .recordTypeBuilder()
                .defaultNamespace(NS)
                .name("recordType")
                .fieldEntry().defineField().name("field1").create().add()
                .fieldEntry().defineField().name("field2").type("RECORD").create().add()
                .fieldEntry().defineField().name("field3").type("LIST<RECORD>").create().add()
                .create();


        Record record = repository
                .recordBuilder()
                .defaultNamespace(NS)
                .recordType("recordType")
                .recordField("field2")
                .recordType("recordType")
                .field("field1", "value 1")
                .set()
                .recordListField("field3")
                .recordType("recordType")
                .field("field1", "value 2")
                .add()
                .field("field1", "value 3")
                .add()
                .field("field1", "value 4")
                .endList()
                .create();

        record = repository.read(record.getId());
        assertEquals("value 1", ((Record) record.getField("field2")).getField("field1"));
        assertEquals("value 2", ((List<Record>) record.getField("field3")).get(0).getField("field1"));
        assertEquals("value 3", ((List<Record>) record.getField("field3")).get(1).getField("field1"));
        assertEquals("value 4", ((List<Record>) record.getField("field3")).get(2).getField("field1"));

        // Calling create on a nested record should not work
        try {
            repository
                    .recordBuilder()
                    .defaultNamespace(NS)
                    .recordType("recordType")
                    .recordField("field2")
                    .recordType("recordType")
                    .field("field1", "value 1")
                    .create();
            fail("expected exception");
        } catch (IllegalStateException e) {
            // expected
        }
    }

    @Test
    public void testRecordValueType() throws Exception {
        String namespace = "testRecordValueType";
        QName rvtRTName = new QName(namespace, "rvtRT");
        QName rtName = new QName(namespace, "rt");
        QName ft1Name = new QName(namespace, "ft1");
        QName ft2Name = new QName(namespace, "ft2");
        QName ft3Name = new QName(namespace, "ft3");
        typeManager.recordTypeBuilder().name(rvtRTName).field(fieldType1.getId(), false).create();
        ValueType rvt = typeManager.getValueType("RECORD<" + rvtRTName.toString() + ">");
        FieldType ft1 = typeManager.createFieldType(typeManager.newFieldType(rvt, ft1Name, Scope.NON_VERSIONED));
        FieldType ft2 = typeManager.createFieldType(typeManager.newFieldType(rvt, ft2Name, Scope.VERSIONED));
        FieldType ft3 = typeManager.createFieldType(typeManager.newFieldType(rvt, ft3Name, Scope.VERSIONED_MUTABLE));
        typeManager.recordTypeBuilder().name(rtName).field(ft1.getId(), false).field(ft2.getId(), false)
                .field(ft3.getId(), false).create();

        Record ft1Value1 = repository.recordBuilder().field(fieldType1.getName(), "ft1abc").build();
        Record ft1Value2 = repository.recordBuilder().field(fieldType1.getName(), "ft1def").build();
        Record ft2Value1 = repository.recordBuilder().field(fieldType1.getName(), "ft2abc").build();
        Record ft2Value2 = repository.recordBuilder().field(fieldType1.getName(), "ft2def").build();
        Record ft3Value1 = repository.recordBuilder().field(fieldType1.getName(), "ft3abc").build();
        Record ft3Value2 = repository.recordBuilder().field(fieldType1.getName(), "ft3def").build();
        Record ft3Value3 = repository.recordBuilder().field(fieldType1.getName(), "ft3xyz").build();

        // Create record
        Record createdRecord =
                repository.recordBuilder().recordType(rtName).field(ft1Name, ft1Value1).field(ft2Name, ft2Value1)
                        .field(ft3Name, ft3Value1).create();
        Record readRecord = repository.read(createdRecord.getId());
        assertEquals("ft1abc", ((Record) readRecord.getField(ft1Name)).getField(fieldType1.getName()));
        assertEquals("ft2abc", ((Record) readRecord.getField(ft2Name)).getField(fieldType1.getName()));
        assertEquals("ft3abc", ((Record) readRecord.getField(ft3Name)).getField(fieldType1.getName()));

        // Update record
        repository.recordBuilder().id(createdRecord.getId()).field(ft1Name, ft1Value2).field(ft2Name, ft2Value2)
                .field(ft3Name, ft3Value2).update();
        readRecord = repository.read(createdRecord.getId());
        assertEquals(new Long(2), readRecord.getVersion());
        assertEquals("ft1def", ((Record) readRecord.getField(ft1Name)).getField(fieldType1.getName()));
        assertEquals("ft2def", ((Record) readRecord.getField(ft2Name)).getField(fieldType1.getName()));
        assertEquals("ft3def", ((Record) readRecord.getField(ft3Name)).getField(fieldType1.getName()));

        readRecord = repository.read(createdRecord.getId(), 1L);
        assertEquals(new Long(1), readRecord.getVersion());
        assertEquals("ft1def", ((Record) readRecord.getField(ft1Name)).getField(fieldType1.getName()));
        assertEquals("ft2abc", ((Record) readRecord.getField(ft2Name)).getField(fieldType1.getName()));
        assertEquals("ft3abc", ((Record) readRecord.getField(ft3Name)).getField(fieldType1.getName()));

        // Update mutable field record
        repository.recordBuilder().id(createdRecord.getId()).version(1L).field(ft3Name, ft3Value3).updateVersion(true)
                .update();
        readRecord = repository.read(createdRecord.getId());
        assertEquals(new Long(2), readRecord.getVersion());
        assertEquals("ft1def", ((Record) readRecord.getField(ft1Name)).getField(fieldType1.getName()));
        assertEquals("ft2def", ((Record) readRecord.getField(ft2Name)).getField(fieldType1.getName()));
        assertEquals("ft3def", ((Record) readRecord.getField(ft3Name)).getField(fieldType1.getName()));

        readRecord = repository.read(createdRecord.getId(), 1L);
        assertEquals(new Long(1), readRecord.getVersion());
        assertEquals("ft1def", ((Record) readRecord.getField(ft1Name)).getField(fieldType1.getName()));
        assertEquals("ft2abc", ((Record) readRecord.getField(ft2Name)).getField(fieldType1.getName()));
        assertEquals("ft3xyz", ((Record) readRecord.getField(ft3Name)).getField(fieldType1.getName()));
    }

    @Test
    public void testRecordNestedInItself() throws Exception {
        String namespace = "testRecordNestedInItself";
        QName rvtRTName = new QName(namespace, "rvtRT");
        QName rtName = new QName(namespace, "rt");
        QName ft1Name = new QName(namespace, "ft1");
        QName ft2Name = new QName(namespace, "ft2");

        typeManager.recordTypeBuilder().name(rvtRTName).field(fieldType1.getId(), false).create();
        ValueType rvt = typeManager.getValueType("RECORD");
        FieldType ft1 = typeManager.createFieldType(typeManager.newFieldType(rvt, ft1Name, Scope.NON_VERSIONED));
        FieldType ft2 = typeManager.createFieldType(typeManager.newFieldType(rvt, ft2Name, Scope.VERSIONED));
        typeManager.recordTypeBuilder().name(rtName).field(ft1.getId(), false).field(ft2.getId(), false).create();

        Record ft1Value1 = repository.recordBuilder().recordType(rvtRTName).field(fieldType1.getName(), "ft1abc")
                .build();

        // Create nested record

        Record record = repository.recordBuilder().recordType(rtName).field(ft1Name, ft1Value1).build();
        record.setField(ft2Name, record);
        try {
            repository.create(record);
            fail("Expecting a Record Exception since a record may not be nested in itself");
        } catch (RecordException expected) {
        }

        // Create with deep nesting

        Record ft2Value2 = repository.recordBuilder().recordType(rvtRTName).field(ft1Name, record).build();
        record.setField(ft2Name, ft2Value2);
        try {
            repository.create(record);
            fail("Expecting a Record Exception since a record may not be nested in itself");
        } catch (RecordException expected) {
        }

        // Update with nested record

        record = repository.recordBuilder().recordType(rtName).field(ft1Name, ft1Value1).build();
        record = repository.create(record);
        record.setField(ft2Name, record); // Nest record in itself
        try {
            repository.update(record);
            fail("Expecting a Record Exception since a record may not be nested in itself");
        } catch (RecordException expected) {
        }
    }

    @Test
    public void testByteArrayValueType() throws Exception {
        FieldType byteArrayValueType = typeManager.createFieldType("BYTEARRAY", new QName("testByteArrayValueType",
                "field1"), Scope.NON_VERSIONED);
        RecordType recordType = typeManager.recordTypeBuilder().defaultNamespace("testByteArrayValueType")
                .name("recordType1")
                .field(byteArrayValueType.getId(), false).create();
        Record record = repository.recordBuilder().defaultNamespace("testByteArrayValueType").recordType("recordType1")
                .field("field1", new ByteArray(Bytes.toBytes("some bytes"))).create();
        Record readRecord = repository.read(record.getId());
        ByteArray readValue = readRecord.getField(new QName("testByteArrayValueType", "field1"));
        assertEquals("some bytes", Bytes.toString(readValue.getBytesUnsafe()));
    }

    @Test
    public void testScannerBasics() throws Exception {
        List<String> fieldValues = new ArrayList<String>();
        for (int i = 'A'; i <= 'Z'; i++) {
            RecordId id = idGenerator.newRecordId("Z" + (char) i);
            Record record = repository.newRecord(id);
            record.setRecordType(recordType1.getName());
            String value = "field 1 - " + (char) i;
            fieldValues.add(value);
            record.setField(fieldType1.getName(), value);
            repository.create(record);
        }

        RecordScan scan = new RecordScan();
        scan.setStartRecordId(idGenerator.newRecordId("ZA"));
        scan.setStopRecordId(idGenerator.newRecordId("ZZ")); // stop row is exclusive

        RecordScanner scanner = repository.getScanner(scan);

        int i = 0;
        Record record;
        while ((record = scanner.next()) != null) {
            assertEquals(record.getField(fieldType1.getName()), fieldValues.get(i));
            i++;
        }
        scanner.close();
        assertEquals("Found 25 records", 25, i);

        // Same using for-each loop
        scanner = repository.getScanner(scan);
        i = 0;
        for (Record result : scanner) {
            assertEquals(result.getField(fieldType1.getName()), fieldValues.get(i));
            i++;
        }
        scanner.close();
        assertEquals("Found 25 records", 25, i);

        // Scan all records, this should give at least 26 results
        scan = new RecordScan();
        scanner = repository.getScanner(scan);
        i = 0;
        while (scanner.next() != null) {
            i++;
        }

        assertTrue("Found at least 26 records", i >= 26);
    }

    @Test
    public void testScannerWithIdRecords() throws Exception {
        RecordId id = idGenerator.newRecordId();
        Record record = repository.newRecord(id);
        record.setRecordType(recordType1.getName());
        record.setField(fieldType1.getName(), "dummy value");
        repository.create(record);

        RecordScan scan = new RecordScan();

        IdRecordScanner scanner = repository.getScannerWithIds(scan);
        final IdRecord next = scanner.next();
        assertNotNull(next);
        assertFalse(next.getFieldIdToNameMapping().isEmpty());
    }

    @Test(expected = ClassCastException.class)
    public void testScannerWithoutIdRecords() throws Exception {
        RecordId id = idGenerator.newRecordId();
        Record record = repository.newRecord(id);
        record.setRecordType(recordType1.getName());
        record.setField(fieldType1.getName(), "dummy value");
        repository.create(record);

        RecordScan scan = new RecordScan();

        // normal Record scanner!
        RecordScanner scanner = repository.getScanner(scan);
        final Record next = scanner.next();
        assertNotNull(next);

        // this cast will fail
        final IdRecord casted = (IdRecord) next;
    }

    @Test
    public void testRecordTypeFilter() throws Exception {
        RecordType rt1 = typeManager.recordTypeBuilder()
                .name("RecordTypeFilter", "rt1")
                .fieldEntry().use(fieldType1).add()
                .create();

        RecordType rt2 = typeManager.recordTypeBuilder()
                .name("RecordTypeFilter", "rt2")
                .fieldEntry().use(fieldType1).add()
                .create();

        // create second version of the record type
        rt2 = typeManager.recordTypeBuilder()
                .name("RecordTypeFilter", "rt2")
                .update();

        assertEquals(new Long(2), rt2.getVersion());

        repository.recordBuilder().recordType(rt1.getName()).field(fieldType1.getName(), "value").create();
        repository.recordBuilder().recordType(rt1.getName()).field(fieldType1.getName(), "value").create();
        repository.recordBuilder().recordType(rt2.getName()).field(fieldType1.getName(), "value").create();
        repository.recordBuilder().recordType(rt2.getName(), 1L).field(fieldType1.getName(), "value").create();

        RecordScan scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rt1.getName()));
        assertEquals(2, countResults(repository.getScanner(scan)));

        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rt2.getName()));
        assertEquals(2, countResults(repository.getScanner(scan)));

        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rt2.getName(), 2L));
        assertEquals(1, countResults(repository.getScanner(scan)));
    }

    @Test
    public void testRecordTypeFilterInstanceOf() throws Exception {
        // The following code creates the following record type hierarchy:
        //
        //          rtA
        //         /   \
        //      rtB     rtC    rtE
        //               |
        //              rtD
        //

        RecordType rtA = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOf", "rtA")
                .fieldEntry().use(fieldType1).add()
                .create();

        RecordType rtB = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOf", "rtB")
                .fieldEntry().use(fieldType1).add()
                .supertype().use(rtA).add()
                .create();

        RecordType rtC = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOf", "rtC")
                .fieldEntry().use(fieldType1).add()
                .supertype().use(rtA).add()
                .create();

        RecordType rtD = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOf", "rtD")
                .fieldEntry().use(fieldType1).add()
                .supertype().use(rtC).add()
                .create();

        RecordType rtE = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOf", "rtE")
                .fieldEntry().use(fieldType1).add()
                .create();

        // Create a record of each type
        repository.recordBuilder().recordType(rtA.getName()).field(fieldType1.getName(), "value").create();
        repository.recordBuilder().recordType(rtB.getName()).field(fieldType1.getName(), "value").create();
        repository.recordBuilder().recordType(rtC.getName()).field(fieldType1.getName(), "value").create();
        repository.recordBuilder().recordType(rtD.getName()).field(fieldType1.getName(), "value").create();
        repository.recordBuilder().recordType(rtE.getName()).field(fieldType1.getName(), "value").create();

        // Check that with "instance of" searches we get the expected number of results for each type in the hierarchy
        RecordScan scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rtA.getName(), RecordTypeFilter.Operator.INSTANCE_OF));
        assertEquals(4, countResults(repository.getScanner(scan)));

        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rtB.getName(), RecordTypeFilter.Operator.INSTANCE_OF));
        assertEquals(1, countResults(repository.getScanner(scan)));

        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rtC.getName(), RecordTypeFilter.Operator.INSTANCE_OF));
        assertEquals(2, countResults(repository.getScanner(scan)));

        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rtD.getName(), RecordTypeFilter.Operator.INSTANCE_OF));
        assertEquals(1, countResults(repository.getScanner(scan)));
    }

    @Test
    public void testRecordTypeFilterInstanceOfRecursionLoop() throws Exception {
        // Create a record type hierarchy which contains some endless loop in it:
        //   - the base hierarchy is C extends from B extends from A
        //   - A also extends from A and from C
        //   - B also extends from C
        //

        // The expected behavior is that it does not go in an endless loop but instead just stops when
        // encountering a loop (i.e. it doesn't throw an exception either)

        RecordType rtA = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfRecursionLoop", "rtA")
                .fieldEntry().use(fieldType1).add()
                .create();

        RecordType rtB = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfRecursionLoop", "rtB")
                .fieldEntry().use(fieldType1).add()
                .create();

        RecordType rtC = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfRecursionLoop", "rtC")
                .fieldEntry().use(fieldType1).add()
                .create();


        rtA = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfRecursionLoop", "rtA")
                .fieldEntry().use(fieldType1).add()
                .supertype().id(rtA.getId()).version(2L).add()
                .supertype().id(rtC.getId()).version(2L).add()
                .update();

        rtB = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfRecursionLoop", "rtB")
                .fieldEntry().use(fieldType1).add()
                .supertype().id(rtA.getId()).version(2L).add()
                .supertype().id(rtC.getId()).version(2L).add()
                .update();

        rtC = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfRecursionLoop", "rtC")
                .fieldEntry().use(fieldType1).add()
                .supertype().id(rtB.getId()).version(2L).add()
                .update();


        // Create a record of each type
        repository.recordBuilder().recordType(rtA.getName()).field(fieldType1.getName(), "value").create();
        repository.recordBuilder().recordType(rtB.getName()).field(fieldType1.getName(), "value").create();
        repository.recordBuilder().recordType(rtC.getName()).field(fieldType1.getName(), "value").create();


        RecordScan scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rtA.getName(), RecordTypeFilter.Operator.INSTANCE_OF));
        assertEquals(3, countResults(repository.getScanner(scan)));

        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rtB.getName(), RecordTypeFilter.Operator.INSTANCE_OF));
        assertEquals(3, countResults(repository.getScanner(scan)));

        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rtB.getName(), RecordTypeFilter.Operator.INSTANCE_OF));
        assertEquals(3, countResults(repository.getScanner(scan)));
    }

    @Test
    public void testRecordTypeFilterInstanceOfVersionSpecifics() throws Exception {
        // The instance-of check for record types is version unaware, or rather, looks at the latest version
        // of each record type. This is explained in the TypeManager.findSubTypes(SchemaId) docs.

        // Below we create a record type A with two versions, and type B extends from the first (non-latest)
        // version, and type C extends from the second (latest) version of type A.
        //
        //        rtA-version1       rtA-version2     rtD-version1
        //          |                 |                |
        //        rtB-version1       rtC-version2     rtC-version1
        //

        RecordType rtA = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfVersionSpecifics", "rtA")
                .fieldEntry().use(fieldType1).add()
                .create();

        rtA = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfVersionSpecifics", "rtA")
                .fieldEntry().use(fieldType1).add()
                .fieldEntry().use(fieldType2).add()
                .update();

        RecordType rtB = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfVersionSpecifics", "rtB")
                .fieldEntry().use(fieldType1).add()
                .supertype().id(rtA.getId()).version(1L).add()
                .create();

        RecordType rtD = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfVersionSpecifics", "rtD")
                .fieldEntry().use(fieldType1).add()
                .create();

        RecordType rtC = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfVersionSpecifics", "rtC")
                .fieldEntry().use(fieldType1).add()
                .supertype().id(rtD.getId()).version(1L).add()
                .create();

        rtC = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfVersionSpecifics", "rtC")
                .fieldEntry().use(fieldType1).add()
                .supertype().id(rtA.getId()).version(2L).add()
                .update();

        repository.recordBuilder().recordType(rtB.getName()).field(fieldType1.getName(), "value").create();
        repository.recordBuilder().recordType(rtC.getName()).field(fieldType1.getName(), "value").create();


        RecordScan scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rtA.getName(), RecordTypeFilter.Operator.INSTANCE_OF));
        assertEquals(2, countResults(repository.getScanner(scan)));

        // Since it is not the latest version of C that extends from D, searching for records that are an
        // instance of D will not return any results, even though C points to the latest version of D (because
        // it is the latest version of C which counts).
        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rtD.getName(), RecordTypeFilter.Operator.INSTANCE_OF));
        assertEquals(0, countResults(repository.getScanner(scan)));
    }

    @Test
    public void testRecordTypeFilterInstanceOfUpdate() throws Exception {
        // Verify correctness of scans with a "instance of" record type filter in case the record type
        // inheritance is updated.
        //
        //  Initial state:
        //
        //       rtA     rtC
        //          \
        //           rtB
        //
        //  After update of rtB:
        //
        //       rtA     rtC
        //              /
        //           rtB
        //

        RecordType rtA = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfUpdate", "rtA")
                .fieldEntry().use(fieldType1).add()
                .create();

        RecordType rtB = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfUpdate", "rtB")
                .fieldEntry().use(fieldType1).add()
                .supertype().use(rtA).add()
                .create();

        RecordType rtC = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfUpdate", "rtC")
                .fieldEntry().use(fieldType1).add()
                .create();

        repository.recordBuilder().recordType(rtB.getName()).field(fieldType1.getName(), "value").create();

        RecordScan scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rtA.getName(), RecordTypeFilter.Operator.INSTANCE_OF));
        assertEquals(1, countResults(repository.getScanner(scan)));

        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rtC.getName(), RecordTypeFilter.Operator.INSTANCE_OF));
        assertEquals(0, countResults(repository.getScanner(scan)));

        rtB = typeManager.recordTypeBuilder()
                .name("RecordTypeFilterInstanceOfUpdate", "rtB")
                .fieldEntry().use(fieldType1).add()
                .supertype().use(rtC).add()
                .update();

        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rtC.getName(), RecordTypeFilter.Operator.INSTANCE_OF));
        assertEquals(1, countResults(repository.getScanner(scan)));

        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rtA.getName(), RecordTypeFilter.Operator.INSTANCE_OF));
        assertEquals(0, countResults(repository.getScanner(scan)));
    }

    @Test
    public void testFieldValueFilter() throws Exception {
        FieldType fieldType =
                typeManager.createFieldType("STRING", new QName("FieldValueFilter", "field"), Scope.NON_VERSIONED);
        RecordType rt = typeManager.recordTypeBuilder()
                .defaultNamespace("FieldValueFilter")
                .name("rt1")
                .fieldEntry()
                .use(fieldType)
                .add()
                .create();

        Record record =
                repository.recordBuilder().recordType(rt.getName()).field(fieldType.getName(), "value1").create();
        repository.recordBuilder().recordType(rt.getName()).field(fieldType.getName(), "value1").create();
        repository.recordBuilder().recordType(rt.getName()).field(fieldType.getName(), "value2").create();
        repository.recordBuilder().recordType(rt.getName()).field(fieldType.getName(), "value2").create();
        repository.recordBuilder().recordType(rt.getName()).field(fieldType.getName(), "value2").create();

        RecordScan scan = new RecordScan();
        scan.setRecordFilter(new FieldValueFilter(fieldType.getName(), "value1"));
        assertEquals(2, countResults(repository.getScanner(scan)));

        scan = new RecordScan();
        scan.setRecordFilter(new FieldValueFilter(fieldType.getName(), "value2"));
        assertEquals(3, countResults(repository.getScanner(scan)));

        scan = new RecordScan();
        scan.setRecordFilter(new FieldValueFilter(fieldType.getName(), CompareOp.NOT_EQUAL, "value1"));
        assertEquals(3, countResults(repository.getScanner(scan)));

        // (At the time of this writing, ...) when non-versioned fields are deleted, a delete marker is
        // written rather than really deleting the field. This delete marker would then also be 'not equal'
        // to the value we search, and we'd get an extra result. This test verifies the implementation takes
        // care of that.
        record.getFieldsToDelete().add(fieldType.getName());
        record.setField(fieldType1.getName(), "whatever");
        record = repository.update(record);

        scan = new RecordScan();
        scan.setRecordFilter(new FieldValueFilter(fieldType.getName(), CompareOp.NOT_EQUAL, "value1"));
        assertEquals(3, countResults(repository.getScanner(scan)));
    }

    @Test
    public void testFilterList() throws Exception {
        FieldType f1 = typeManager.createFieldType("STRING", new QName("FilterList", "field1"), Scope.NON_VERSIONED);
        FieldType f2 = typeManager.createFieldType("STRING", new QName("FilterList", "field2"), Scope.NON_VERSIONED);

        RecordType rt = typeManager.recordTypeBuilder().defaultNamespace("FilterList").name("rt")
                .fieldEntry().use(f1).add().fieldEntry().use(f2).add().create();

        repository.recordBuilder()
                .recordType(rt.getName())
                .field(f1.getName(), "A")
                .field(f2.getName(), "B")
                .create();

        repository.recordBuilder()
                .recordType(rt.getName())
                .field(f1.getName(), "A")
                .field(f2.getName(), "C")
                .create();

        repository.recordBuilder()
                .recordType(rt.getName())
                .field(f1.getName(), "D")
                .field(f2.getName(), "B")
                .create();

        repository.recordBuilder()
                .recordType(rt.getName())
                .field(f1.getName(), "F")
                .create();

        // Test f1=A and f2=B
        RecordScan scan = new RecordScan();
        RecordFilterList filterList = new RecordFilterList();
        filterList.addFilter(new FieldValueFilter(f1.getName(), "A"));
        filterList.addFilter(new FieldValueFilter(f2.getName(), "B"));
        scan.setRecordFilter(filterList);
        assertEquals(1, countResults(repository.getScanner(scan)));

        // Test f1=A or f2=B
        scan = new RecordScan();
        filterList = new RecordFilterList(RecordFilterList.Operator.MUST_PASS_ONE);
        filterList.addFilter(new FieldValueFilter(f1.getName(), "A"));
        filterList.addFilter(new FieldValueFilter(f2.getName(), "B"));
        scan.setRecordFilter(filterList);
        assertEquals(3, countResults(repository.getScanner(scan)));

        // Test f1=A and (f2=B or f2=C)
        scan = new RecordScan();
        RecordFilterList filterList2 = new RecordFilterList(RecordFilterList.Operator.MUST_PASS_ONE);
        filterList2.addFilter(new FieldValueFilter(f2.getName(), "B"));
        filterList2.addFilter(new FieldValueFilter(f2.getName(), "C"));
        filterList = new RecordFilterList();
        filterList.addFilter(new FieldValueFilter(f1.getName(), "A"));
        filterList.addFilter(filterList2);
        scan.setRecordFilter(filterList);
        assertEquals(2, countResults(repository.getScanner(scan)));

        // Test f1=F and f2=Z
        scan = new RecordScan();
        filterList = new RecordFilterList();
        filterList.addFilter(new FieldValueFilter(f1.getName(), "F"));
        filterList.addFilter(new FieldValueFilter(f2.getName(), "Z"));
        scan.setRecordFilter(filterList);
        assertEquals(0, countResults(repository.getScanner(scan)));

        // Test f1=F and (f2=Z with filterIfMissing=false)
        scan = new RecordScan();
        filterList = new RecordFilterList();
        filterList.addFilter(new FieldValueFilter(f1.getName(), "F"));
        FieldValueFilter fvf = new FieldValueFilter(f2.getName(), "Z");
        fvf.setFilterIfMissing(false);
        filterList.addFilter(fvf);
        scan.setRecordFilter(filterList);
        assertEquals(1, countResults(repository.getScanner(scan)));
    }

    @Test
    public void testScanWithReturnFields() throws Exception {
        FieldType f1 = typeManager.createFieldType("STRING", new QName("ReturnFieldsScan", "f1"), Scope.NON_VERSIONED);
        FieldType f2 = typeManager.createFieldType("STRING", new QName("ReturnFieldsScan", "f2"), Scope.NON_VERSIONED);
        FieldType f3 = typeManager.createFieldType("STRING", new QName("ReturnFieldsScan", "f3"), Scope.NON_VERSIONED);
        FieldType f4 = typeManager.createFieldType("STRING", new QName("ReturnFieldsScan", "f4"), Scope.NON_VERSIONED);

        RecordType rt = typeManager.recordTypeBuilder().defaultNamespace("ReturnFieldsScan").name("rt")
                .fieldEntry().use(f1).add()
                .fieldEntry().use(f2).add()
                .fieldEntry().use(f3).add()
                .fieldEntry().use(f4).add()
                .create();

        repository.recordBuilder()
                .recordType(rt.getName())
                .field(f1.getName(), "A")
                .field(f2.getName(), "B")
                .field(f3.getName(), "C")
                .create();


        // Test ALL filter
        RecordScan scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rt.getName()));
        scan.setReturnFields(new ReturnFields(ReturnFields.Type.ALL));
        Record record = repository.getScanner(scan).next();
        assertEquals(3, record.getFields().size());

        // Test NONE filter
        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rt.getName()));
        scan.setReturnFields(new ReturnFields(ReturnFields.Type.NONE));
        record = repository.getScanner(scan).next();
        assertEquals(0, record.getFields().size());

        // Test ENUM filter
        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rt.getName()));
        scan.setReturnFields(new ReturnFields(f1.getName(), f2.getName()));
        record = repository.getScanner(scan).next();
        assertEquals(2, record.getFields().size());
        assertTrue(record.hasField(f1.getName()));
        assertTrue(record.hasField(f2.getName()));
        assertFalse(record.hasField(f3.getName()));

        // Test scanning on filtered field, should not work
        scan = new RecordScan();
        RecordFilterList filterList = new RecordFilterList();
        filterList.addFilter(new RecordTypeFilter(rt.getName()));
        filterList.addFilter(new FieldValueFilter(f1.getName(), "A"));
        scan.setRecordFilter(filterList);
        // without ReturnFields, we get a result
        assertNotNull(repository.getScanner(scan).next());
        // with ReturnFields that doesn't include f1, we don't get a result
        scan.setReturnFields(new ReturnFields(f2.getName()));
        // TODO disabled this test as it was sometimes failing, and sometimes not
        //      In the cases where it failed, it did bring up the row which is
        //      correct according to the filters, but which we didn't expect to
        //      receive because a filter was applied on a non-read column.
        //      The conclusion could be that while HBase can't guarantee the filter
        //      will work always, it sometimes will work? Needs more investigation.
        //assertNull(repository.getScanner(scan).next());
    }

    /**
     * Tests if record type is set when different settings of returnFields is used.
     */
    @Test
    public void testScanWithReturnFieldsRecordType() throws Exception {
        String ns = "ReturnFieldsScan-RecordType";
        FieldType f1 = typeManager.createFieldType("STRING", new QName(ns, "f1"), Scope.NON_VERSIONED);
        FieldType f2 = typeManager.createFieldType("STRING", new QName(ns, "f2"), Scope.NON_VERSIONED);
        FieldType f3 = typeManager.createFieldType("STRING", new QName(ns, "f3"), Scope.NON_VERSIONED);
        FieldType f4 = typeManager.createFieldType("STRING", new QName(ns, "f4"), Scope.NON_VERSIONED);

        RecordType rt = typeManager.recordTypeBuilder().defaultNamespace(ns).name("rt")
                .fieldEntry().use(f1).add()
                .fieldEntry().use(f2).add()
                .fieldEntry().use(f3).add()
                .fieldEntry().use(f4).add()
                .create();

        repository.recordBuilder()
                .recordType(rt.getName())
                .field(f1.getName(), "A")
                .field(f2.getName(), "B")
                .field(f3.getName(), "C")
                .create();


        // Test ALL filter
        RecordScan scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rt.getName()));
        scan.setReturnFields(new ReturnFields(ReturnFields.Type.ALL));
        Record record = repository.getScanner(scan).next();
        assertNotNull(record.getRecordTypeName());
        assertEquals(ns, record.getRecordTypeName().getNamespace());

        // Test NONE filter
        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rt.getName()));
        scan.setReturnFields(new ReturnFields(ReturnFields.Type.NONE));
        record = repository.getScanner(scan).next();
        assertNotNull(record.getRecordTypeName());
        assertEquals(ns, record.getRecordTypeName().getNamespace());

        // Test ENUM filter
        scan = new RecordScan();
        scan.setRecordFilter(new RecordTypeFilter(rt.getName()));
        scan.setReturnFields(new ReturnFields(f1.getName(), f2.getName()));
        record = repository.getScanner(scan).next();
        assertNotNull(record.getRecordTypeName());
        assertEquals(ns, record.getRecordTypeName().getNamespace());
    }

    @Test
    public void testPrefixScans() throws Exception {
        repository.recordBuilder()
                .id("PrefixScanTest")
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        repository.recordBuilder()
                .id("PrefixScanTest-suffix1")
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        repository.recordBuilder()
                .id("PrefixScanTest-suffix2")
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        repository.recordBuilder()
                .id("QPrefixScanTest")
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        RecordScan scan = new RecordScan();
        scan.setStartRecordId(idGenerator.newRecordId("PrefixScanTest"));

        RecordScanner scanner = repository.getScanner(scan);
        assertEquals(idGenerator.newRecordId("PrefixScanTest"), scanner.next().getId());
        assertEquals(idGenerator.newRecordId("PrefixScanTest-suffix1"), scanner.next().getId());
        assertEquals(idGenerator.newRecordId("PrefixScanTest-suffix2"), scanner.next().getId());
        // the scanner would run till the end of the table
        assertNotNull(scanner.next());
        scanner.close();

        scan.setRecordFilter(new RecordIdPrefixFilter(idGenerator.newRecordId("PrefixScanTest")));
        scanner = repository.getScanner(scan);
        assertEquals(idGenerator.newRecordId("PrefixScanTest"), scanner.next().getId());
        assertEquals(idGenerator.newRecordId("PrefixScanTest-suffix1"), scanner.next().getId());
        assertEquals(idGenerator.newRecordId("PrefixScanTest-suffix2"), scanner.next().getId());
        // due to the prefix filter, the scanner stops once there are no records left with the same prefix
        assertNull(scanner.next());
        scanner.close();

        //
        // When using UUID record ID's, prefix scans make less sense, except for retrieving
        // variants
        //
        RecordId uuid = idGenerator.newRecordId();
        RecordId varid1 = idGenerator.newRecordId(uuid, ImmutableMap.of("lang", "en", "year", "1999"));
        RecordId varid2 = idGenerator.newRecordId(uuid, ImmutableMap.of("lang", "fr"));

        repository.recordBuilder()
                .id(uuid)
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        repository.recordBuilder()
                .id(varid1)
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        repository.recordBuilder()
                .id(varid2)
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        scan = new RecordScan();
        scan.setStartRecordId(uuid);
        scan.setRecordFilter(new RecordIdPrefixFilter(uuid));
        scanner = repository.getScanner(scan);
        assertEquals(uuid, scanner.next().getId());
        assertEquals(varid1, scanner.next().getId());
        assertEquals(varid2, scanner.next().getId());
        assertNull(scanner.next());
        scanner.close();
    }

    /**
     * At the time of this writing, record deletion was implemented by marking records
     * as deleted. We need to make sure these are skipped when scanning, and that
     * this doesn't conflict with custom filters.
     */
    @Test
    public void testScannerAndDeletedRecords() throws Exception {
        for (int i = 0; i < 5; i++) {
            RecordId id = idGenerator.newRecordId("ScanDeleteTest-" + i);
            Record record = repository.newRecord(id);
            record.setRecordType(recordType1.getName());
            String value = "dummy";
            record.setField(fieldType1.getName(), value);
            repository.create(record);
        }

        RecordScan scan = new RecordScan();
        scan.setStartRecordId(idGenerator.newRecordId("ScanDeleteTest-"));
        scan.setRecordFilter(new RecordIdPrefixFilter(idGenerator.newRecordId("ScanDeleteTest-")));

        RecordScanner scanner = repository.getScanner(scan);
        assertEquals(5, countResults(scanner));
        scanner.close();

        // This is to make sure the filtering of deleted records also works when we don't
        // specify a filter on our scan.
        RecordScan singleScan = new RecordScan();
        singleScan.setStartRecordId(idGenerator.newRecordId("ScanDeleteTest-0"));
        singleScan.setStopRecordId(idGenerator.newRecordId("ScanDeleteTest-0"));

        scanner = repository.getScanner(singleScan);
        assertEquals(1, countResults(scanner));
        scanner.close();

        // Delete the records, verify the new scanner results
        repository.delete(idGenerator.newRecordId("ScanDeleteTest-0"));

        scanner = repository.getScanner(scan);
        assertEquals(4, countResults(scanner));
        scanner.close();

        scanner = repository.getScanner(singleScan);
        assertEquals(0, countResults(scanner));
        scanner.close();
    }

    private int countResults(RecordScanner scanner) throws RepositoryException, InterruptedException {
        int i = 0;
        while (scanner.next() != null) {
            i++;
        }
        return i;
    }

    @Test
    public void testVariantScansWithKeysUSER() throws Exception {
        doTestVariantScansWithKeys(idGenerator.newRecordId("VariantScanWithKeysTest"));
    }

    @Test
    public void testVariantScansWithKeysUUID() throws Exception {
        doTestVariantScansWithKeys(idGenerator.newRecordId());
    }

    private void doTestVariantScansWithKeys(RecordId master) throws Exception {
        final RecordId variant = idGenerator.newRecordId(master, ImmutableMap.of("key1", "value1", "key2", "value2"));
        final RecordId variantWithOtherValues =
                idGenerator.newRecordId(master, ImmutableMap.of("key1", "other-value-1", "key2", "other-value-2"));
        final RecordId extendedVariant =
                idGenerator.newRecordId(master, ImmutableMap.of("key1", "value1", "key2", "value2", "key3", "value3"));

        repository.recordBuilder()
                .id(master)
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        repository.recordBuilder()
                .id(variant)
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        repository.recordBuilder()
                .id(extendedVariant)
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        repository.recordBuilder()
                .id(variantWithOtherValues)
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        {
            // master scan
            final RecordScan scan = new RecordScan();
            scan.setRecordFilter(new RecordVariantFilter(master, new HashMap<String, String>()));
            RecordScanner scanner = repository.getScanner(scan);
            assertEquals(master, scanner.next().getId());
            assertNull(scanner.next());
            scanner.close();
        }

        {
            // variant scan
            final HashMap<String, String> variantProperties = new HashMap<String, String>();
            variantProperties.put("foo", null);
            variantProperties.put("bar", null);

            final RecordScan scan = new RecordScan();
            scan.setRecordFilter(new RecordVariantFilter(variant.getMaster(), variantProperties));
            RecordScanner scanner = repository.getScanner(scan);
            assertNull(scanner.next()); // it doesn't match anything
            scanner.close();
        }

        {
            // variant scan for something completely different
            final HashMap<String, String> variantProperties = new HashMap<String, String>();
            variantProperties.put("key1", null);
            variantProperties.put("key2", null);

            final RecordScan scan = new RecordScan();
            scan.setRecordFilter(new RecordVariantFilter(variant.getMaster(), variantProperties));
            RecordScanner scanner = repository.getScanner(scan);
            assertEquals(variant, scanner.next().getId());
            assertEquals(variantWithOtherValues, scanner.next().getId());
            assertNull(scanner.next()); // it doesn't match the extendedVariant!
            scanner.close();
        }

        {
            // extended variant scan
            final HashMap<String, String> variantProperties = new HashMap<String, String>();
            variantProperties.put("key1", null);
            variantProperties.put("key2", null);
            variantProperties.put("key3", null);

            final RecordScan scan = new RecordScan();
            scan.setRecordFilter(
                    new RecordVariantFilter(extendedVariant.getMaster(), variantProperties));
            RecordScanner scanner = repository.getScanner(scan);
            assertEquals(extendedVariant, scanner.next().getId());
            assertNull(scanner.next());
            scanner.close();
        }
    }

    @Test
    public void testVariantScansWithKeysAndValues() throws Exception {
        final RecordId master = idGenerator.newRecordId("VariantScanWithKeysAndValuesTest");
        final RecordId variant = idGenerator.newRecordId(master, ImmutableMap.of("key1", "value1", "key2", "value2"));
        final RecordId variantWithOtherValues =
                idGenerator.newRecordId(master, ImmutableMap.of("key1", "other-value-1", "key2", "value2"));
        final RecordId extendedVariant =
                idGenerator.newRecordId(master, ImmutableMap.of("key1", "value1", "key2", "value2", "key3", "value3"));

        repository.recordBuilder()
                .id(master)
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        repository.recordBuilder()
                .id(variant)
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        repository.recordBuilder()
                .id(extendedVariant)
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        repository.recordBuilder()
                .id(variantWithOtherValues)
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "foo")
                .create();

        {
            // variant scan with all values
            final HashMap<String, String> variantProperties = new HashMap<String, String>();
            variantProperties.put("key1", "value1");
            variantProperties.put("key2", "value2");

            final RecordScan scan = new RecordScan();
            scan.setRecordFilter(new RecordVariantFilter(variant.getMaster(), variantProperties));
            RecordScanner scanner = repository.getScanner(scan);
            assertEquals(variant, scanner.next().getId());
            assertNull(scanner.next()); // it doesn't match the other variants
            scanner.close();
        }

        {
            // variant scan with specific value for key2 only
            final HashMap<String, String> variantProperties = new HashMap<String, String>();
            variantProperties.put("key1", null);
            variantProperties.put("key2", "value2");

            final RecordScan scan = new RecordScan();
            scan.setRecordFilter(new RecordVariantFilter(variant.getMaster(), variantProperties));
            RecordScanner scanner = repository.getScanner(scan);
            assertEquals(variant, scanner.next().getId());
            assertEquals(variantWithOtherValues, scanner.next().getId()); // has same value for key2
            assertNull(scanner.next()); // it doesn't match the other variants
            scanner.close();
        }

        {
            // variant scan with specific value for key1 only
            final HashMap<String, String> variantProperties = new HashMap<String, String>();
            variantProperties.put("key1", "value1");
            variantProperties.put("key2", null);

            final RecordScan scan = new RecordScan();
            scan.setRecordFilter(new RecordVariantFilter(variant.getMaster(), variantProperties));
            RecordScanner scanner = repository.getScanner(scan);
            assertEquals(variant, scanner.next().getId());
            assertNull(scanner.next()); // it doesn't match the other variants
            scanner.close();
        }
    }

    @Test
    public void testMetadataSimpleStoreLoad() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        record.setField(fieldType1.getName(), "field value");

        Metadata metadata = new MetadataBuilder().value("field1", "value1").value("field2", "value2").build();

        record.setMetadata(fieldType1.getName(), metadata);
        assertEquals(2, record.getMetadataMap().get(fieldType1.getName()).getMap().size());

        record = repository.create(record);

        // Check state of returned record object
        Metadata returnedMetadata = record.getMetadata(fieldType1.getName());
        assertNotNull(returnedMetadata);
        assertEquals(2, returnedMetadata.getMap().size());
        assertEquals("value1", returnedMetadata.get("field1"));
        assertEquals("value2", returnedMetadata.get("field2"));

        // Check state when freshly reading record
        record = repository.read(record.getId());

        assertEquals(1, record.getMetadataMap().size());
        Metadata readMetadata = record.getMetadataMap().get(fieldType1.getName());
        assertNotNull(readMetadata);
        assertEquals(2, readMetadata.getMap().size());
        assertEquals("value1", readMetadata.get("field1"));
        assertEquals("value2", readMetadata.get("field2"));
    }

    @Test
    public void testMetadataAllTypes() throws Exception {
        // This test verifies that the change detection logic works correctly for all data types

        List<Pair> testValues = new ArrayList<Pair>();
        testValues.add(Pair.create("string1", "string2"));
        testValues.add(Pair.create(new Integer(1), new Integer(2)));
        testValues.add(Pair.create(new Long(1), new Long(2)));
        testValues.add(Pair.create(new Float(1f), new Float(2f)));
        testValues.add(Pair.create(new Double(1d), new Double(2d)));
        testValues.add(Pair.create(Boolean.TRUE, Boolean.FALSE));
        testValues.add(Pair.create(new ByteArray("A".getBytes()), new ByteArray("B".getBytes())));

        for (Pair testValue : testValues) {
            String ctx = "testing " + testValue.getV1() + " of type " + testValue.getV1().getClass().getName();

            Record record = repository.newRecord();
            record.setRecordType(recordType1.getName());
            record.setField(fieldType2.getName(), new Integer(1));
            record.setMetadata(fieldType2.getName(), new MetadataBuilder()
                    .object("somefield", testValue.getV1())
                    .build());
            record = repository.create(record);

            // test the type of the metadata has been retained
            assertEquals(ctx, testValue.getV1().getClass(),
                    record.getMetadata(fieldType2.getName()).getMap().get("somefield").getClass());

            // do dummy update, should not create new version
            //  (apparently, this also works fine for the float & double tests)
            record.setMetadata(fieldType2.getName(), new MetadataBuilder()
                    .object("somefield", testValue.getV1())
                    .build());
            record = repository.update(record);
            assertEquals(ctx, 1L, record.getVersion().longValue());

            // do update, should create new version
            record.setMetadata(fieldType2.getName(), new MetadataBuilder()
                    .object("somefield", testValue.getV2())
                    .build());
            record = repository.update(record);
            assertEquals(ctx, 2L, record.getVersion().longValue());

            // test the type of the metadata has been retained
            record = repository.read(record.getId());
            assertEquals(ctx, testValue.getV1().getClass(),
                    record.getMetadata(fieldType2.getName()).getMap().get("somefield").getClass());
        }
    }

    @Test
    public void testMetadataPartialUpdate() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        record.setField(fieldType1.getName(), "field value");

        Metadata metadata = new MetadataBuilder()
                .value("field1", "value1")
                .value("field2", "value2")
                .value("field3", "value3").build();

        record.setMetadata(fieldType1.getName(), metadata);
        record = repository.create(record);
        RecordId recordId = record.getId();

        // Now update metadata:
        //   - field1 is left unchanged and not specified
        //   - field2 is updated
        //   - field3 is deleted
        //   - a new field4 is added
        metadata = new MetadataBuilder()
                .value("field2", "value2a")
                .delete("field3")
                .value("field4", "value4").build();

        record = repository.newRecord(recordId);
        record.setMetadata(fieldType1.getName(), metadata);
        record = repository.update(record);

        // Check state of returned record object
        Metadata returnedMetadata = record.getMetadata(fieldType1.getName());
        assertNotNull(returnedMetadata);
        assertEquals(2, returnedMetadata.getMap().size());
        assertEquals("value2a", returnedMetadata.get("field2"));
        assertEquals("value4", returnedMetadata.get("field4"));
        assertEquals(0, returnedMetadata.getFieldsToDelete().size());

        // Check state when freshly reading record
        record = repository.read(record.getId());

        Metadata readMetadata = record.getMetadataMap().get(fieldType1.getName());
        assertNotNull(readMetadata);
        assertEquals(3, readMetadata.getMap().size());
        assertEquals("value1", readMetadata.get("field1"));
        assertEquals("value2a", readMetadata.get("field2"));
        assertEquals("value4", readMetadata.get("field4"));
    }

    @Test
    public void testMetadataOnUndefinedField() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        record.setField(fieldType1.getName(), "field value");
        // Set metadata on a field which does not have a value, such metadata should be ignored
        record.setMetadata(fieldType2.getName(), new MetadataBuilder().value("field1", "value1").build());

        record = repository.create(record);

        // Check state of returned record object
        assertNull(record.getMetadata(fieldType1.getName()));
        assertNull(record.getMetadata(fieldType2.getName()));
        assertEquals(0, record.getMetadataMap().size());

        // Check state of freshly read record
        record = repository.read(record.getId());
        assertNull(record.getMetadata(fieldType1.getName()));
        assertNull(record.getMetadata(fieldType2.getName()));
        assertEquals(0, record.getMetadataMap().size());
    }

    @Test
    public void testMetadataVersionedField() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        record.setField(fieldType2.getName(), new Integer(5));

        record.setMetadata(fieldType2.getName(), new MetadataBuilder().value("field1", "value1").build());
        record = repository.create(record);

        record = repository.read(record.getId());

        Metadata readMetadata = record.getMetadataMap().get(fieldType2.getName());
        assertNotNull(readMetadata);
        assertEquals(1, readMetadata.getMap().size());
        assertEquals("value1", readMetadata.get("field1"));
    }

    @Test
    public void testMetadataUpdateFieldAndMetadata() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        record.setField(fieldType2.getName(), new Integer(5));
        record.setMetadata(fieldType2.getName(), new MetadataBuilder()
                .value("field1", "value1")
                .value("field2", "value2").build());
        record = repository.create(record);

        // do update to field and metadata
        record.setField(fieldType2.getName(), new Integer(6));
        record.setMetadata(fieldType2.getName(), new MetadataBuilder()
                .value("field1", "value1a")
                .value("field3", "value3").build()); // note that we leave field2 unchanged, this tests the merging
        // of old and new metadata
        record = repository.update(record);

        // validate state of returned record object
        assertEquals(2L, record.getVersion().longValue());
        assertEquals(new Integer(6), record.getField(fieldType2.getName()));

        Metadata metadata = record.getMetadataMap().get(fieldType2.getName());
        assertNotNull(metadata);
        assertEquals(2, metadata.getMap().size());
        assertEquals("value1a", metadata.get("field1"));
        assertEquals("value3", metadata.get("field3"));

        // validate state of read record
        record = repository.read(record.getId());
        assertEquals(2L, record.getVersion().longValue());
        assertEquals(new Integer(6), record.getField(fieldType2.getName()));

        metadata = record.getMetadataMap().get(fieldType2.getName());
        assertNotNull(metadata);
        assertEquals(3, metadata.getMap().size());
        assertEquals("value1a", metadata.get("field1"));
        assertEquals("value2", metadata.get("field2"));
        assertEquals("value3", metadata.get("field3"));
    }

    @Test
    public void testMetadataUpdateFieldOnly() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        record.setField(fieldType2.getName(), new Integer(5));
        record.setMetadata(fieldType2.getName(), new MetadataBuilder().value("field1", "value1").build());
        record = repository.create(record);
        RecordId recordId = record.getId();

        // update only field value, metadata should not be lost be inherited from previous record state
        record = repository.newRecord();
        record.setId(recordId);
        record.setField(fieldType2.getName(), new Integer(7));
        record = repository.update(record);

        // validate state of read record
        record = repository.read(record.getId());
        assertEquals(2L, record.getVersion().longValue());
        assertEquals(new Integer(7), record.getField(fieldType2.getName()));

        Metadata readMetadata = record.getMetadataMap().get(fieldType2.getName());
        assertNotNull(readMetadata);
        assertEquals(1, readMetadata.getMap().size());
        assertEquals("value1", readMetadata.get("field1"));
    }

    @Test
    public void testMetadataUpdateMetadataOnly() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        record.setField(fieldType2.getName(), new Integer(5));
        record.setMetadata(fieldType2.getName(), new MetadataBuilder()
                .value("field1", "value1")
                .value("field2", "value2").build());
        record = repository.create(record);
        RecordId recordId = record.getId();

        // Update only metadata -- should create new version
        record = repository.newRecord();
        record.setId(recordId);
        record.setMetadata(fieldType2.getName(), new MetadataBuilder().value("field1", "value1a").build());
        record = repository.update(record);

        Metadata metadata;

        // validate state of returned record
        assertEquals(2L, record.getVersion().longValue());
        metadata = record.getMetadataMap().get(fieldType2.getName());
        assertNotNull(metadata);
        assertEquals(1, metadata.getMap().size());
        assertEquals("value1a", metadata.get("field1"));

        // validate state of read record
        record = repository.read(record.getId());
        assertEquals(2L, record.getVersion().longValue());
        metadata = record.getMetadataMap().get(fieldType2.getName());
        assertNotNull(metadata);
        assertEquals(2, metadata.getMap().size());
        assertEquals("value1a", metadata.get("field1"));
        assertEquals("value2", metadata.get("field2"));
    }

    @Test
    public void testMetadataNoUpdate() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        record.setField(fieldType2.getName(), new Integer(1));
        record.setMetadata(fieldType2.getName(), new MetadataBuilder().value("field1", "value1").build());
        record = repository.create(record);

        assertEquals(1, record.getVersion().longValue());
        assertEquals(1, record.getMetadata(fieldType2.getName()).getMap().size());

        // resubmit the same record object, this should not cause an update
        record = repository.update(record);
        assertEquals(1, record.getVersion().longValue());

        // Delete a non-existing field from the metadata, this should also not cause an update
        record.setMetadata(fieldType2.getName(), new MetadataBuilder().delete("field2").build());
        record = repository.update(record);
        assertEquals(1, record.getVersion().longValue());

        // Once more with an empty metadata object
        record.setMetadata(fieldType2.getName(), new MetadataBuilder().build());
        record = repository.update(record);
        assertEquals(1, record.getVersion().longValue());

        // But if we do a real update, we should get a new version
        record.setMetadata(fieldType2.getName(), new MetadataBuilder().value("field1", "value1a").build());
        record = repository.update(record);
        assertEquals(2, record.getVersion().longValue());
    }

    @Test
    public void testMetadataOnDeletedField() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        record.setField(fieldType1.getName(), "field value");
        record.setField(fieldType2.getName(), new Integer(1));
        record.setMetadata(fieldType1.getName(), new MetadataBuilder().value("field1", "value1").build());
        record = repository.create(record);
        RecordId recordId = record.getId();

        record = repository.newRecord(recordId);
        record.delete(fieldType1.getName(), true);
        record.setMetadata(fieldType1.getName(), new MetadataBuilder().value("field1", "value1a").build());
        record = repository.update(record);

        // validate state of returned record
        assertFalse(record.hasField(fieldType1.getName()));
        assertNull(record.getMetadata(fieldType1.getName()));

        // validate state of read record
        record = repository.read(recordId);
        assertFalse(record.hasField(fieldType1.getName()));
        assertNull(record.getMetadata(fieldType1.getName()));
        assertNotNull(record.getField(fieldType2.getName()));
    }

    @Test
    public void testMetadataSuperfluousUpdates() throws Exception {
        // This test verifies that we don't write metadata if there are zero fields in it

        // Record with empty Metadata object
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        record.setField(fieldType1.getName(), "field value");
        record.setField(fieldType2.getName(), new Integer(1));
        record.setMetadata(fieldType1.getName(), new MetadataBuilder().build());
        record = repository.create(record);

        // validate state of read record
        record = repository.read(record.getId());
        assertNull(record.getMetadata(fieldType1.getName()));

        // Record with Metadata object containing only delets
        record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        record.setField(fieldType1.getName(), "field value");
        record.setField(fieldType2.getName(), new Integer(1));
        record.setMetadata(fieldType1.getName(), new MetadataBuilder().delete("field1").build());
        record = repository.create(record);

        // validate state of read record
        record = repository.read(record.getId());
        assertNull(record.getMetadata(fieldType1.getName()));
    }

    @Test
    public void testMetadataNotSupportedOnVersionedMutableFields() throws Exception {
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        record.setField(fieldType1.getName(), "field value");
        record.setField(fieldType3.getName(), Boolean.TRUE);
        record.setMetadata(fieldType3.getName(), new MetadataBuilder().value("field1", "value1").build());
        try {
            record = repository.create(record);
            fail("Expected an exception trying to set metadata on a versioned-mutable field");
        } catch (Exception e) {
            assertTrue(e.getMessage().contains("Field metadata is currently not supported for versioned-mutable fields."));
        }
    }

    @Test
    public void testMetadataNotSupportedOnBlobFields() throws Exception {
        String[] types = new String[]{"BLOB", "LIST<BLOB>", "LIST<PATH<BLOB>>"};

        for (int i = 0; i < types.length; i++) {
            FieldType blobField = typeManager
                    .createFieldType("BLOB", new QName("metadata-blob", "blob" + i), Scope.NON_VERSIONED);

            Blob blob = new Blob("text/plain", 5L, "foo");
            OutputStream os = repository.getOutputStream(blob);
            os.write("12345".getBytes());
            os.close();

            Record record = repository.newRecord();
            record.setRecordType(recordType1.getName());
            record.setField(blobField.getName(), blob);
            record.setMetadata(blobField.getName(), new MetadataBuilder().value("field1", "value1").build());
            try {
                record = repository.create(record);
                fail("Expected an exception trying to set metadata on a blob field");
            } catch (Exception e) {
                assertTrue(e.getMessage().contains("Field metadata is currently not supported for BLOB fields."));
            }
        }
    }

    @Test
    public void testFieldValueFilterWhenFieldHasMetadata() throws Exception {
        // The purpose of this test is to verify that the FieldValueFilter also works when there
        // is metadata for the field (metadata is stored in the same cell as the value, and should be ignored
        // by the FieldValueFilter)
        Record record = repository.newRecord();
        record.setRecordType(recordType1.getName());
        record.setField(fieldType1.getName(), "stop drinking coke");
        record.setMetadata(fieldType1.getName(), new MetadataBuilder().value("field1", "foobar").build());
        repository.create(record);

        RecordScan scan = new RecordScan();
        scan.setRecordFilter(new FieldValueFilter(fieldType1.getName(), "stop drinking coke"));
        assertEquals(1, countResults(repository.getScanner(scan)));
    }

    @Test
    public void testMetadataViaRecordBuilder() throws Exception {
        Record record = repository.recordBuilder()
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "hi")
                .metadata(fieldType1.getName(), new MetadataBuilder().value("x", "y").build())
                .create();

        record = repository.read(record.getId());
        assertEquals("y", record.getMetadata(fieldType1.getName()).get("x"));
    }

    @Test
    public void testMetadataFieldsToDeleteNotStored() throws Exception {
        Record record = repository.recordBuilder()
                .recordType(recordType1.getName())
                .field(fieldType1.getName(), "hi")
                .metadata(fieldType1.getName(), new MetadataBuilder().value("x", "y").delete("z").build())
                .create();

        record = repository.read(record.getId());
        assertEquals(0, record.getMetadata(fieldType1.getName()).getFieldsToDelete().size());
    }
}
TOP

Related Classes of org.lilyproject.repository.impl.test.AbstractRepositoryTest

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.