1

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);
    }

}
wolpers
  • 121
  • 1
  • 5

1 Answers1

2

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));
Vyacheslav Lukianov
  • 1,913
  • 8
  • 12
  • The use case I had in mind was to be able to iterate over a large dataset ([Sequence CRDT](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type#Sequence_CRDTs)) in-order. Also, when inserting key/value pairs I would need to be able to look left and right for existing keys to generate a key that fits between. Hence the range search would need to work for my use case to work. – wolpers May 15 '17 at 07:25
  • Range search would work as expected (in the same way as it does with predefined bindings) if your custom binding is a monotonically increasing function, i.e. for any two comparable objects o1, o2 (o1 <= o2) it produces two ByteIterables b1, b2, such that b1 <= b2, and vice versa: for any b1, b2 (b1 <= b2) it produces objects o1, o2, such that o1 <= o2. – Vyacheslav Lukianov May 15 '17 at 09:27
  • I am missing something here. I just cannot see how the Xodus Cursor can keep up with the intended ordering if it cannot actually use/find my custom ByteIterable implementation. I updated the question with a code snippet. It would be great if you could have a look and tell me how to extend this to make the Cursor walk the Store in the desired order. Thanks! – wolpers May 16 '17 at 08:48
  • Sorry to bother again, but I don't see where the ambiguity you mention originates from. Isn't the order defined by my ByteIterable.compareTo(..) implementation, only? At least this is what I expected. Regarding the String representation of Integers in my snippet, this is just an example. My quest is really about the use of rather complex keys where I define the ordering as well as the encoding of the keys as bytes. But this also means that the Xodus Environment needs to instantiate my implementation when reading already stored data, no? – wolpers May 18 '17 at 12:58
  • Under the hood only binary representation of keys and values is used for search and navigation, not ByteIterable.compareTo(). Frankly, it doesn't make sense to implement ByteIterable.compareTo() at all. Serialization itself defines the order, that's why you don't have to install a custom binding, deserialization isn't required for comparisons. A complex key can be thought as a tuple of primary comparables, and its binary representation is just a stream of binary representations of the comparables, probably split by a separator (e.g., zero byte). Usually, such a binding is an app-level stuff. – Vyacheslav Lukianov May 18 '17 at 15:07