Search code examples
xodus

Custom ComparableBinding implementation


Is it possible to implement a custom ComparableBinding/ByteIterable combination that provides its own ordering? And how would I register this with the system? Also, would it be safe to not implement the ByteIterable.subIterable(final int offset, final int length) method when used for keys, only? There would be no valid subiterable in my use case, as this would break the ordering.

The following TestStore.test() method would not be enough to make the Cursor move in ascending order because the assert statement at the bottom fails. It works when using the build-in IntegerBinding.intToEntry(index) to generate keys, though:

import jetbrains.exodus.ArrayByteIterable;
import jetbrains.exodus.ByteIterable;
import jetbrains.exodus.ByteIterator;
import org.jetbrains.annotations.NotNull;

import java.nio.charset.Charset;

public class TestKey implements ByteIterable {

    private final int value;
    private final byte[] bytes;

    public TestKey(int value) {
        this.value = value;
        this.bytes = Integer.toString(value).getBytes(Charset.forName("utf-8"));
    }

    @Override
    public int compareTo(@NotNull ByteIterable o) {
        return Integer.compare(value, ((TestKey)o).value);
    }

    @Override
    public ByteIterator iterator() {
        return new ArrayByteIterable(bytes).iterator();
    }

    @Override
    public byte[] getBytesUnsafe() {
        return bytes;
    }

    @Override
    public int getLength() {
        return bytes.length;
    }

    @Override
    public @NotNull ByteIterable subIterable(int offset, int length) {
        throw new UnsupportedOperationException("subIterable");
    }
}



import jetbrains.exodus.ByteIterable;
import jetbrains.exodus.bindings.IntegerBinding;
import jetbrains.exodus.bindings.StringBinding;
import jetbrains.exodus.env.Cursor;
import jetbrains.exodus.env.Environment;
import jetbrains.exodus.env.Environments;
import jetbrains.exodus.env.Store;
import jetbrains.exodus.env.StoreConfig;
import jetbrains.exodus.env.Transaction;
import jetbrains.exodus.env.TransactionalExecutable;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.util.Arrays;
import java.util.UUID;

public class TestStore {


    private Store store;
    private Environment environment;

    public TestStore(File folder) {
        environment = Environments.newContextualInstance(folder);
        environment.executeInTransaction(new TransactionalExecutable() {
            @Override
            public void execute(@NotNull Transaction txn) {
                store = environment.openStore(
                        UUID.randomUUID().toString(),
                        StoreConfig.WITHOUT_DUPLICATES,
                        txn,
                        true);
            }
        });
    }

    public void test() {

        int count = 1000;

        int[] orig = new int[count];
        int[] iterated = new int[count];

        for(int i = 0; i < count; i++) {
            final int index = i;
            environment.executeInTransaction(new TransactionalExecutable() {
                @Override
                public void execute(@NotNull Transaction txn) {
                    orig[index] = index;
                    store.put(txn,
                            new TestKey(index),
                       //     IntegerBinding.intToEntry(index),
                            StringBinding.stringToEntry(Integer.toString(index))
                    );
                }
            });
        }


        environment.executeInTransaction(new TransactionalExecutable() {
            @Override
            public void execute(@NotNull Transaction txn) {
                int offset = 0;
                try(Cursor cursor = store.openCursor(txn)) {
                    while(cursor.getNext()) {
                        ByteIterable key = cursor.getKey();
                        ByteIterable value = cursor.getValue();
                        iterated[offset++] = Integer.parseInt(StringBinding.entryToString(value));
                    }
                }
            }
        });

        assert Arrays.equals(orig, iterated);
    }

}

Solution

  • If using the Environments API, there is no need to care of order of keys/values since the API accepts data only as ByteIterables instances, so it's agnostic to how the ByteIterables are generated. Also there is no need to register a binding somehow, it can be defined in the application. The only drawback of custom ordering may be range search producing kinda weird results.

    As for the subIterable() method, take a look at FixedLengthByteIterable. If you use custom ByteIterables as keys only, it's safe to not implement the method, though there are no explicit guarantees on that in the API.

    As for your test, the TestKey class defines an ambiguous order. On the one hand, it defines order of the keys as natural integer order. On the other, in binary representation it's ordered by string representation of natural integers. If you need to store string representation of integers, pad it with zeros to some accuracy. In that case, you don't even have to declare a class for keys. E.g., for int key, 10-digit ByteIterables (keyEntry) can be calculated as follows:

    final DecimalFormat format = (DecimalFormat) NumberFormat.getIntegerInstance();
    format.applyPattern("0000000000");
    final ByteIterable keyEntry = StringBinding.stringToEntry(format.format(key));