Basic ItemSpace Example

Here is a ‘hello world’ code sample to show the fast, efficient, simple, lowest-level API. There is also a higher-level ‘Map-based’ API, and all data can be printed and parsed as JSON.

/*
Copyright (c) 2019 Boiler Bay Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package com.infinitydb.examples;

import java.io.File;
import java.io.IOException;

import com.infinitydb.BinaryLongObjectInputStream;
import com.infinitydb.BinaryLongObjectOutputStream;
import com.infinitydb.Cu;
import com.infinitydb.InfinityDB;
import com.infinitydb.ItemSpace;
import com.infinitydb.ItemSubspace;

/**
 * Demonstrate the lowest-level InfinityDB operations, based on an 'ItemSpace'
 * data model. An ItemSpace is extremely simple, providing only a few simple
 * basic methods. The result of the simplicity is performance, space efficiency,
 * and the flexibility to represent a wide range of superimposed data models.
 * 
 * An ItemSpace is logically only a sorted set of 'Items', each Item being a
 * series of 1 to 1665 chars. The ItemSpace has no other state. An Item that is
 * a prefix of another comes first.
 * 
 * Items are stored, retrieved, and manipulated in a 'Cu' cursor, which contains
 * exactly one Item. It has no other state. (A Cu actually contains only a
 * private single char[] array 1665 chars long, with a current length from 0 to
 * 1665.)
 * 
 * At the next higher logical level, the Items represent a sequence of primitive
 * values of 12 types. All of the Java primitives such as a long, double, String
 * and so on have corresponding InfinityDB types, but there are some other
 * non-Java ones. Each primitive or other type in an Item is called a
 * 'component'. Each Item contains a variable number of components of any
 * mixture of types.
 * 
 * Each component type has a permanent standard efficient variable-length
 * self-delimiting representation in the Item. Applications never need to
 * understand the representations. Items are stored variable-length with common
 * prefixes between adjacent Items compressed out in storage.
 * 
 * Primitives include long, String, double, float, boolean, small byte[], small
 * char[], ByteString, EntityClass, Attribute, Index, and the Infinity constant.
 * The small arrays sort with length being most significant, but ByteStrings are
 * packed binary that sort like String. EntityClass and Attribute are for
 * optional 'metadata' in the Items to help them describe themselves. Index is
 * for huge arrays. The small arrays are used with Index to represent BLOBs and
 * CLOBs. Index plus Strings are good for representing texts.
 * 
 * There is also a Map-based API. Any ItemSpace can be wrapped with an
 * InfinityDBMap to provide an extended
 * java.util.concurrent.ConcurrentNavigableMap view. InfinityDB is actually more
 * than an ItemSpace, providing transactions and so on.
 */
public class BasicItemSpaceExample {

    // This is for the maximum of the data blocks in memory.
    static long MAX_CACHE_SIZE = 100_000_000;

    // Where the data is stored.
    InfinityDB db;
    File dbFile;

    public static void main(String... args) {
        new BasicItemSpaceExample();
    }

    BasicItemSpaceExample() {
        try {
            dbFile = File.createTempFile("BasicItemSpaceExample_", ".infdb");
            // true means writeable.
            db = InfinityDB.create(dbFile.toString(), true, MAX_CACHE_SIZE);

            helloWorld();
            protectedPrefixLength();
            setsOfItemsSharingAPrefix();
            tableApproach();
            itemSubspace();
            blob();

            // Optionally make all changes durable, or rollBack().
            db.commit();
            db.close();
            dbFile.delete();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    /*
     * Minimal ItemSpace usage. An InfinityDB instance is an example of an
     * ItemSpace.
     */
    void helloWorld() throws IOException {
        try (Cu cu = Cu.alloc()) {

            // Mutations

            // Create a minimal Item in cu. This is a single String component.
            cu.append("hello world");

            // Put the Item in the database
            db.insert(cu);
            // Remove from the database.
            db.delete(cu);
            // restore to database
            db.insert(cu);
            // Put in a two-component Item
            cu.append("again");
            db.insert(cu);

            // Retrievals.
            // You can use >, <, >=, or <= for retrievals.

            // Restore the cu to emptiness. This is before the first Item.
            cu.clear();
            db.next(cu);
            // The nearest greater Item following the zero-length Item is
            // "hello world".
            System.out.println("Nearest greater= " + cu);
            db.next(cu);
            // Now the cu is at "hello world" "again"
            System.out.println("Nearest greater= " + cu);
            // Nothing happens at the end of the Items - not an error,
            // but false is returned.
            db.next(cu);
            // "hello world" "again"
            System.out.println("Nearest greater= " + cu);

            // The nearest greater or equal Item from "h" is "hello world"
            cu.clear().append("h");
            db.first(cu);
            // "hello world"
            System.out.println("Nearest greater or equal: " + cu);
            // Nothing happens, return true
            db.first(cu);
            // "hello world"
            System.out.println("Nearest greater or equal: " + cu);

            // The nearest lesser Item is "hello world" "again"
            cu.clear().append("hello world").append("again")
                    .append("yet again");
            db.previous(cu);
            // "hello world" "again"
            System.out.println("Nearest lesser Item = " + cu);
            db.previous(cu);
            // "hello world"
            System.out.println("Nearest lesser Item = " + cu);
            // No error, but cu is unchanged, and return false.
            db.previous(cu);
            System.out.println("Nearest lesser Item = " + cu);

            // The nearest lesser or equal Item is "hello world" "again"
            // This Item is not present but is bigger than the last.
            cu.clear().append("hello world").append("again")
                    .append("yet again");
            db.last(cu);
            // "hello world", "again"
            System.out.println("Nearest lesser or equal Item = " + cu);
            // no change, return true
            db.last(cu);
            // "hello world", "again"
            System.out.println("Nearest lesser or equal Item = " + cu);

            // Show the Item's standard encoded form - not necessary
            dumpItemInHex(cu);
        }
    }

    /**
     * Multi-component Items can be retrieved using a 'protected prefix' length
     * to limit the motion.
     */
    void protectedPrefixLength() throws IOException {
        try (Cu cu = Cu.alloc()) {

            // Empty the database
            db.clear();

            cu.clear().append("hello world").append(1);
            // Now the Cu contains two components: a string and a long
            // It prints like "hello world" 1.
            System.out.println("two component Item = " + cu);
            db.insert(cu);

            // Get the offset in the Item after the "hello world"
            int off = cu.skipComponent(0);
            // Off is the length of the "hello world" component.
            // Truncate cu after that so we can show retrieving the 1.
            cu.setLength(off);
            // The off is a 'protected prefix length'. The cu before that is not
            // modified
            if (db.next(cu, off)) {
                // true means we found a greater Item having that prefix.
                System.out.println("two component Item retrieved = " + cu);
                /*
                 * Print the 1 by itself. We get it as a 'component' without
                 * knowing its type, so it comes out as a Long.
                 */
                System.out.println(
                        "the retrieved long by itself = "
                                + cu.componentAt(off));
                // If we expect a long, this will fail if not
                long one = cu.longAt(off);
                System.out.println(
                        "the retrieved long by itself = " + one);
            }
        }
    }

    void setsOfItemsSharingAPrefix()
            throws IOException {
        try (Cu cu = Cu.alloc()) {

            // Create a database with two Items with a common prefix

            db.clear();
            cu.clear().append("hello world").append("suffix one");
            db.insert(cu);
            cu.clear().append("hello world").append("suffix two");
            db.insert(cu);

            // Iterate the suffixes

            // Get the length of the prefix over which to iterate. Often this is
            // already available.
            int off = cu.skipComponent(0);
            // Truncate to the prefix over which to iterate. Often this is
            // already available.
            cu.setLength(off);
            // The off is a 'protected prefix', so the "hello world" prefix
            // doesn't change. This prints "suffix one", then "suffix two"
            while (db.next(cu, off)) {
                // suffixes are in sorted order
                System.out.println("Two suffixes = " + cu.componentAt(off));
            }

            // Add an Item
            cu.clear().append("hello world").append("suffix three");
            db.insert(cu);

            // Three suffixes are printed
            for (cu.setLength(off); db.next(cu, off);) {
                System.out.println("Three suffixes = " + cu.componentAt(off));
            }

            // Iterate starting after "suffix one"
            cu.setLength(off).append("suffix one");
            while (db.next(cu, off)) {
                System.out.println("After suffix one = " + cu.componentAt(off));
            }

            // Delete all of the suffixes of the "hello world" component at
            // once, quickly
            cu.setLength(off);
            db.deleteSubspace(cu);
            // Show results. None are printed
            // Iterate the suffixes. The off is a 'protected prefix', so the
            // "hello world" prefix doesn't change
            cu.setLength(off).append("suffix one");
            while (db.next(cu, off)) {
                System.out.println("No suffixes = " + cu.componentAt(off));
            }
        }
    }

    /**
     * A table.
     * 
     * We have a set of Items that represent a credit cards. We use a String
     * table name, and a Long for the card number key, followed by some
     * components for a 'tuple'. The tuple part does not include the key,
     * however. Also, a 'Names' index is maintained.
     * 
     * This technique does not take full advantage of the flexibility of an
     * ItemSpace, but it is simple. It has the usual structural rigidity of a
     * normalized relational system, but our special approach using EntityClass
     * and Attribute 'embedded metadata' component types is more extensible.
     * Also, the ItemSpace can represent trees, sets, and much more.
     */
    void tableApproach() throws IOException {
        try (Cu cu = Cu.alloc()) {

            db.clear();

            // First we show the low-level operations approach, then later
            // 'tuples'.

            insert("CreditCards", 7355_1474_7155_3157L,
                    "313 My street", 75933, "John Doe");
            insert("CreditCards", 1756_3185_1870_3185L,
                    "12 Other street", 13579, "Jane Doe");

            // Print the retrieved Item in default 'tokenized' form.
            cu.clear().append("CreditCards").append(7355_1474_7155_3157L);
            if (db.next(cu, cu.length())) {
                System.out.println("retrieved tuple: " + cu);
            }
            // Get the the name column
            System.out.println("name=" + getComponent(cu, 4));

            // An easier way. Use Object[] tuples

            // Get a card tuple and replace it changed
            Object[] card = getSuffixTuple("CreditCards", 7355_1474_7155_3157L);
            card[2] = "Jack Doe";
            // Must delete, or we get multiple tuples sharing a key
            deleteSubspace("CreditCards", 7355_1474_7155_3157L);
            insert("CreditCards", 7355_1474_7155_3157L, card);

            // Create an inverted index on the second field, which is name
            createIndex("CreditCards", "Names", 2);
            printItems(db, "indexed db ");

            // Even easier: wrap the work into relevant methods:

            putCard(9135_1696_3156_8778L, "313 Yet Another street", 75933,
                    "John Doe");
            deleteCard(7355_1474_7155_3157L);
            printItems(db, "updated db ");

            System.out.println(
                    "card by name: " + getCardNumberByName("John Doe"));
        }
    }

    // Get an Object[] with the components after the table name and card number
    Object[] getCard(Long cardNumber) throws IOException {
        return getSuffixTuple("CreditCards", cardNumber);
    }

    // Add a card tuple, maintaining an 'index' of the tuples on the name
    // column.
    void putCard(Long cardNumber, Object... tuple)
            throws IOException {
        insert("CreditCards", cardNumber, tuple);
        insert("Name", tuple[2], cardNumber);
    }

    // Delete a card, including the index on the name column
    void deleteCard(Long cardNumber) throws IOException {
        Object[] oldTuple = getSuffixTuple("CreditCards", cardNumber);
        deleteSubspace("CreditCards", cardNumber);
        delete("Names", oldTuple[2], cardNumber);
    }

    Long getCardNumberByName(String name) throws IOException {
        Object[] tuple = getSuffixTuple("Names", name);
        return tuple == null ? null : (Long)tuple[0];
    }

    // Get an Object[] suffix following a prefix.
    Object[] getSuffixTuple(Object... prefix) throws IOException {
        try (Cu cu = Cu.alloc(prefix)) {
            int prefixLength = cu.length();
            return db.next(cu, prefixLength) ? cu.compositeAt(prefixLength)
                    : null;
        }
    }

    /*
     * Create an index on any column. Use component 0 as table name, component 1
     * as key.
     */
    void createIndex(String tableName, String indexName,
            int column) throws IOException {
        try (Cu cu = Cu.alloc(tableName)) {
            int prefixLength = cu.length();
            while (db.next(cu, prefixLength)) {
                insert(indexName, getComponent(cu, 2 + column),
                        getComponent(cu, 1));
            }
        }
    }

    // Earlier versions don't have insert(Object...)
    void insert(Object... o) throws IOException {
        db.insert((Object)o);
    }

    // Earlier versions don't have delete(Object...)
    void delete(Object... o) throws IOException {
        db.delete((Object)o);
    }

    // Earlier versions don't have deleteSubspace(Object...)
    void deleteSubspace(Object... o) throws IOException {
        db.deleteSubspace((Object)o);
    }

    /*
     * How many components are in Cu?
     */
    static int getComponentCount(Cu cu) {
        for (int off = 0, i = 0;; i++, off = cu.skipComponent(off)) {
            if (off == cu.length())
                return i;
        }
    }

    /*
     * Get the n'th component from cu.
     */
    static Object getComponent(Cu cu, int n) {
        for (int off = 0, i = 0; off < cu.length();
                i++, off = cu.skipComponent(off)) {
            if (i == n)
                return cu.componentAt(off);
        }
        return null;
    }

    /*
     * Change the n'th component to v.
     */
    static void putComponent(Cu cu, int n, Object v) {
        try (Cu cuTemp = Cu.alloc()) {
            for (int off = 0, i = 0; off < cu.length();
                    i++, off = cu.skipComponent(off)) {
                cuTemp.append(i == n ? v : cu.componentAt(off));
            }
            cu.copyFrom(cuTemp);
        }
    }

    /**
     * An example of an ItemSpace that 'wraps' another. There are also
     * DeltaItemSpace, VolatileItemSpace, IndirectItemSpace and more plus
     * ItemSpaceScanner.
     */
    void itemSubspace() throws IOException {
        try (Cu cu = Cu.alloc()) {

            // Inserting Object[] allocates a Cu and appends the elements as
            // components like a 'tuple'
            db.clear();
            insert("locations", "Santa Cruz", "CA", "USA");
            insert("locations", "San Jose", "CA", "USA");
            insert("locations", "San Francisco", "CA", "USA");

            // Wrap the given ItemSpace such that only some suffixes are visible
            ItemSpace locationsDb = new ItemSubspace(db, "locations");
            // Print the suffixes alone. Inserting or deleting prepends the
            // prefixes.
            printItems(locationsDb, "locations = ");
        }
    }

    // Just iterate the ItemSpace and print it.
    static void printItems(ItemSpace itemSpace, String message)
            throws IOException {
        try (Cu cu = Cu.alloc()) {
            // The prefixLength for next() defaults to 0.
            while (itemSpace.next(cu)) {
                System.out.println(message + cu);
            }
        }
    }

    /**
     * A Binary Long OBject or 'BLOB' or a character long object or 'CLOB' can
     * be put under any prefix. There is no size limit. It is stored using an
     * 'Index' component and a byte[] component after that in each Item. An
     * 'Index' component is basically a special kind of long.
     */
    void blob() throws IOException {
        try (Cu cu = Cu.alloc()) {
            db.clear();
            // We can put a blob anywhere at all with a suitable prefix.
            cu.clear().append("the blob's prefix");
            BinaryLongObjectOutputStream blobOut =
                    new BinaryLongObjectOutputStream(db, cu);
            blobOut.write(42);
            blobOut.close();
            BinaryLongObjectInputStream blobIn =
                    new BinaryLongObjectInputStream(db, cu);
            System.out.println("blob result = " + blobIn.read());
        }
    }

    /**
     * Show the Item in its permanent standard encoded form. This is never
     * needed in practice. The component encodings are variable-length,
     * self-typed, and self-delimiting. Byte, short, int and long all are stored
     * as longs, but small longs are compressed.
     */
    static void dumpItemInHex(Cu cu) {
        System.out
                .println("The Item in internal encoded form (never needed) = ");
        // The cu.charAt(i) is special - it has no primitive type but instead
        // prints
        // the char at the offset. Use Strings instead for storing chars as
        // primitives.
        for (int i = 0; i < cu.length(); i++) {
            System.out.print(Integer.toHexString(cu.charAt(i)) + " ");
        }
        System.out.println();
    }
}