All Downloads are FREE. Search and download functionalities are using the official Maven repository.

oracle.kv.impl.api.table.IndexImpl Maven / Gradle / Ivy

Go to download

NoSQL Database Server - supplies build and runtime support for the server (store) side of the Oracle NoSQL Database.

The newest version!
/*-
 * Copyright (C) 2011, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This file was distributed by Oracle as part of a version of Oracle NoSQL
 * Database made available at:
 *
 * http://www.oracle.com/technetwork/database/database-technologies/nosqldb/downloads/index.html
 *
 * Please see the LICENSE file included in the top-level directory of the
 * appropriate version of Oracle NoSQL Database for a copy of the license and
 * additional information.
 */

package oracle.kv.impl.api.table;

import static oracle.kv.impl.api.table.TableJsonUtils.DESC;
import static oracle.kv.impl.api.table.TableJsonUtils.FIELDS;
import static oracle.kv.impl.api.table.TableJsonUtils.NAME;
import static oracle.kv.impl.api.table.TableJsonUtils.NAMESPACE;
import static oracle.kv.impl.api.table.TableJsonUtils.TABLE;
import static oracle.kv.impl.api.table.TableJsonUtils.TYPE;
import static oracle.kv.impl.api.table.TableJsonUtils.TYPES;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import oracle.kv.impl.admin.IllegalCommandException;
import oracle.kv.impl.api.table.TablePath.StepInfo;
import oracle.kv.impl.api.table.TablePath.StepKind;
import oracle.kv.impl.query.compiler.CompilerAPI;
import oracle.kv.impl.util.JsonUtils;
import oracle.kv.impl.util.SerialVersion;
import oracle.kv.table.FieldDef;
import oracle.kv.table.FieldDef.Type;
import oracle.kv.table.FieldRange;
import oracle.kv.table.FieldValue;
import oracle.kv.table.Index;
import oracle.kv.table.IndexKey;
import oracle.kv.table.RecordValue;

import com.sleepycat.bind.tuple.TupleInput;
import com.sleepycat.bind.tuple.TupleOutput;
import org.codehaus.jackson.map.ObjectWriter;
import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.JsonNodeFactory;
import org.codehaus.jackson.node.ObjectNode;

/**
 * Implementation of the Index interface.  Instances of this class are created
 * and associated with a table when an index is defined.  It contains the index
 * metdata as well as many utility functions used in serializing and
 * deserializing index keys.
 *
 * An index can be viewed as a sorted table of N + 1 columns. Each of the first
 * N columns has an indexable atomic type (one of the numeric types or string
 * or enum). The last column stores serialized primary keys "pointing" to rows
 * in the undelying base table.
 *
 * The rows of the index are computed as follows:
 *
 * - Each index column C (other than the last one) is associated with a path
 *   expr Pc, which when evaluated on a base-table row R produces one or more
 *   indexable atomic values. Let Pc(R) be the *set* of values produced by Pc
 *   on a row R (Pc(R) may produce duplicate values, but the duplicates do not
 *   participate in index creation). If Pc is a path expr that may produce
 *   multiple values from a row, we say that C is a "multi-key" column, and
 *   the whole index is a "multi-key" index.
 *
 * - Each Pc may have at most one step, call it MK, that may produce multiple
 *   values. MK is a [] or _key step whose input is an array or map value from
 *   a row. We denote with MK-Pc the path expr that contains the steps from the
 *   start of Pc up to (and including) MK, and with R-Pc the remaining steps in
 *   Pc.
 *
 * - An index may contain more than one multi-key column, but the path exprs
 *   for all of these columns must all have the same MK-Pc.
 *
 * - Then, conceptually, the index rows are computed by a query like this:
 *
 *   select a.Pc1 as C1, c.R-Pc2 as C2, c.R-Pc3 as C3, primary_key(a) as PK
 *   from A as a, a.MK-Pc as c
 *   order by a.Pc1, c.R-Pc2, c.R-Pc3
 *
 *   In the above query, we assumed the index has 4 columns (N = 3), two of
 *   which (C2 and C3) are multi-key columns sharing the MK-Pc path. If there
 *   are no multi-key columns, the query is simpler:
 *
 *   select a.Pc1 as C1, a.Pc2 as C2, a.Pc3 as C3, primary_key(a) as PK
 *   from A as a,
 *   order by a.Pc1, a.Pc2, a.Pc3
 */
public class IndexImpl implements Index, Serializable {

    private static final String KEY_TAG = "_key";

    private static final String DOT_BRACKETS = ".[]";

    private static final long serialVersionUID = 1L;

    /* The serial version for v4.3 and before */
    static final int INDEX_VERSION_V0 = 0;

    /*
     * Index serial v1:
     * - Add EMTPY_INDICATOR
     * - NULL_INDICATOR is changed to 0x7f
     */
    static final int INDEX_VERSION_V1 = 1;

    /* The current serial version used in the index */
    static final int INDEX_VERSION_CURRENT = INDEX_VERSION_V1;

    /*
     * The indicator used in key serialization for values that are not
     * one of the special values (SQL NULL, json null, or EMPTY)
     */
    static final byte NORMAL_VALUE_INDICATOR = 0x00;

    /*
     * The indicator representing the EMPTY value (added since v1):
     * The EMPTY value is used if an index path, when evaluated on a
     * table as a query path expr, returns an empty result. For example:
     *  - The index field is an empty array or map.
     *  - The index field is map., the specific-key is missing
     *    from the map value.
     *  - If the index field is a sub field of json field, the sub field is
     *    missing from the json value and the json value is not sql null or
     *    json null.
     */
    static final byte EMPTY_INDICATOR = 0x7D;

    /* The indicator for JSON null used in key serialization (added since v1) */
    static final byte JSON_NULL_INDICATOR = 0x7E;

    /* The indicator for null value used in key serialization (v1) */
    static final byte NULL_INDICATOR_V1 = 0x7F;

    /* The indicator for null value used in key serialization */
    static final byte NULL_INDICATOR_V0 = 0x01;

    /* For testing purpose */
    final static String INDEX_NULL_DISABLE = "test.index.null.disable";

    final static String INDEX_SERIAL_VERSION = "test.index.serial.version";

    /* the index name */
    private final String name;

    /* the (optional) index description, user-provided */
    private final String description;

    /* the associated table */
    private final TableImpl table;

    /*
     * The stringified path exprs that define the index columns. In the case of
     * map indexes a path expr may contain the special steps "._key" and ".[]"
     * to distinguish between the 3 possible ways of indexing a map: (a) all
     * the keys (using a _key step), (b) all the values (using a [] step), or
     * (c) the value of a specific map entry (using the specific key of the
     * entry we want indexed). In case of array indexes, the ".[]" is used.
     *
     * The string format described above is the "old" format.  The newFields
     * list below stores the same strings in the "new" foramt, which is the
     * one that is used in the DDL systax. Specifically, for maps the new format
     * uses ".keys()" and ".values()" instead of "._key", ".[]", and for arrays
     * it uses "[]" instead of ".[]".
     *
     * The new format was introduced in 4.3 and all the data model code was
     * modified to use this format. However, the old format is still here
     * to satisfy the backwards compatimity requirement. The difficult case
     * is when a new server must serialize (via java) and send an IndexImpl
     * to an old client, which of course, expects and undestands the old
     * format only. For this reason, during (de)serialization the old format
     * is used always, and newFields is declared transient.
     *
     * In 4.4, newFields (and newAnnotations) were made persistent. this.fields
     * and this.annotations remained peristent in order to handle the old-client
     * problem mentioned above, as well as existing indexes created by older
     * server versions. newFields was made persistent because in 4.4 steps may
     * be quoted, and the quotes must be preserved when an IndexImpl is
     * serialized to disk (putting the quotes into this.fields would not work
     * because the index would then not work at an old client, since the old
     * client would not be removing the quotes). Notice also that with the
     * introduction of json indexes in 4.4, this.fields alone is not enough
     * to correctly initialize the transient state of IndexImpl, because in
     * the case of json index paths there is no schema to determine whether
     * a .[] step appearing in this.fields should be converted to .values()
     * or to [].
     */
    private final List fields;


    /* status is used when an index is being populated to indicate readiness */
    private IndexStatus status;

    private final Map annotations;

    /*
     * properties of the index; used by text indexes only; can be null.
     */
    private final Map properties;

    /*
     * Indicates whether this index includes an indicator byte in each field
     * of its binary keys. Indicator bytes were added in 4.2. For older,
     * existing indexes this will be false. For new indexes it is true, unless
     * test-specific state is set to turn this feature off.
     */
    private final boolean isNullSupported;

    /*
     * The version of the index. This data member was introduced in 4.4.
     */
    private final int indexVersion;

    /*
     * This data member was made persistent in 4.4.
     */
    private List newFields;

    /*
     * The declared types for the json index paths (if an index path is not
     * json, its corresponding entry in the list will be null).
     * This data member was introduced in 4.4.
     */
    private final List types;

    /*
     * This data member was made persistent in 4.4
     */
    private Map newAnnotations;

    /*
     * transient version of the index column definitions, materialized as
     * IndexField for efficiency. It is technically final but is not because it
     * needs to be initialized in readObject after deserialization.
     */
    private transient List indexFields;

    /*
     * transient indication of whether this is a multiKeyMapIndex.  This is
     * used for serialization/deserialization of map indexes.  It is
     * technically final but is not because it needs to be initialized in
     * readObject after deserialization.
     */
    private transient boolean isMultiKeyMapIndex;

    private transient boolean isPointIndex;

    private transient int geoFieldPos = -1;

    /*
     * transient RecordDefImpl representing the definition of IndexKeyImpl
     * instances for this index.
     */
    private transient RecordDefImpl indexKeyDef;

    /*
     * transient RecordDefImpl representing a full index entry, including the
     * primary key fields
     */
    private transient RecordDefImpl indexEntryDef;

    public enum IndexStatus {
        /** Index is transient */
        TRANSIENT() {
            @Override
            public boolean isTransient() {
                return true;
            }
        },

        /** Index is being populated */
        POPULATING() {
            @Override
            public boolean isPopulating() {
                return true;
            }
        },

        /** Index is populated and ready for use */
        READY() {
            @Override
            public boolean isReady() {
                return true;
            }
        };

        /**
         * Returns true if this is the {@link #TRANSIENT} type.
         * @return true if this is the {@link #TRANSIENT} type
         */
        public boolean isTransient() {
            return false;
        }

        /**
         * Returns true if this is the {@link #POPULATING} type.
         * @return true if this is the {@link #POPULATING} type
         */
        public boolean isPopulating() {
            return false;
        }

        /**
         * Returns true if this is the {@link #READY} type.
         * @return true if this is the {@link #READY} type
         */
        public boolean isReady() {
            return false;
        }
    }

    public IndexImpl(String name,
                     TableImpl table,
                     List fields,
                     List types,
                     String description) {
    	this(name, table, fields, types, null, null, description);
    }

    public IndexImpl(String name, TableImpl table, List fields,
                     String description) {
    	this(name, table, fields, null, null, null, description);
    }

    /* Constructor for Full Text Indexes. */
    public IndexImpl(String name,
                     TableImpl table,
                     List fields,
                     List types,
                     Map annotations,
                     Map properties,
                     String description) {
    	this.name = name;
    	this.table = table;
    	this.newFields = fields;
    	this.types = types;
    	this.newAnnotations = annotations;
    	this.properties = properties;
    	this.description = description;
    	status = IndexStatus.TRANSIENT;
    	isNullSupported = areIndicatorBytesEnabled();
    	indexVersion = getIndexVersion();

    	/* validate initializes indexFields and the other transient state */
    	validate();

    	this.fields = new ArrayList(newFields.size());

    	if (newAnnotations != null) {
    	    this.annotations = new HashMap();
    	} else {
    	    this.annotations = null;
    	}

    	translateToOldFields();
    }

    private void translateToOldFields() {

        StringBuilder sb = new StringBuilder();

        for (IndexField field : indexFields) {

            for (int s = 0; s < field.numSteps(); ++s) {

                StepInfo si = field.getStepInfo(s);
                String step = si.step;

                if (si.kind == StepKind.BRACKETS) {
                    sb.append(TableImpl.BRACKETS);

                } else if (si.kind == StepKind.VALUES) {
                    sb.append(TableImpl.BRACKETS);

                } else if (si.kind == StepKind.KEYS) {
                    sb.append(KEY_TAG);

                } else {
                    /*
                     * If the step was a quoted one, the quotes are removed
                     * in the old path format. Leaving the quotes there would
                     * definitely make the index not work at an old client.
                     * Without the quotes, the index may or may not work at
                     * an old client, depending on what is inside the quotes.
                     */
                    sb.append(step);
                }

                if (s < field.numSteps() - 1) {
                    sb.append(NameUtils.CHILD_SEPARATOR);
                }
            }

            String oldField = sb.toString();
            sb.delete(0, sb.length());

            fields.add(oldField);

            if (newAnnotations != null) {
                String annotation =
                    newAnnotations.get(newFields.get(field.getPosition()));
                if (annotation != null) {
                    annotations.put(oldField, annotation);
                }
            }
        }
    }

    @Override
    public TableImpl getTable() {
        return table;
    }

    @Override
    public String getName()  {
        return name;
    }

    /**
     * Returns true if this index indexes all the entries (both keys and
     * associated data) of a map, with the key field appearing before any
     * of the data fields. This is info needed by the query optimizer.
     */
    public boolean isMapBothIndex() {

        List ipaths = getIndexFields();
        boolean haveMapKey = false;
        boolean haveMapValue = false;

        if (!isMultiKeyMapIndex) {
            return false;
        }

        for (IndexField ipath : ipaths) {

            if (ipath.isMapKeys()) {
                haveMapKey = true;
                if (haveMapValue) {
                    return false;
                }
            } else if (ipath.isMapValues()) {
                haveMapValue = true;
                if (haveMapKey) {
                    break;
                }
            }
        }

        return (haveMapKey && haveMapValue);
    }

    /*
     * Returns a potentially modifyable list of the string paths for the
     * indexed fields.
     */
    @Override
    public List getFields() {
        if (newFields == null) {
            initTransientState();
        }
        return newFields;
    }

    public IndexField getIndexPath(int pos) {
        return getIndexFields().get(pos);
    }

    public IndexField getIndexPath(String fieldName) {
        return getIndexFields().get(getIndexKeyDef().getFieldPos(fieldName));
    }

    boolean fieldMayHaveSpecialValue(int pos) {
        return getIndexPath(pos).mayHaveSpecialValue();
    }

    /**
     * Returns an list of the fields that define a text index.
     * These are in order of declaration which is significant.
     *
     * @return the field names
     */
    public List getFieldsWithAnnotations() {

        if (! isTextIndex()) {
            throw new IllegalStateException
                ("getFieldsWithAnnotations called on non-text index");
        }

        final List fieldsWithAnnotations =
            new ArrayList(getFields().size());

        Map anns = getAnnotationsInternal();

        for (String field : getFields()) {
            fieldsWithAnnotations.add(
               new AnnotatedField(field, anns.get(field)));
        }
        return fieldsWithAnnotations;
    }

    Map getAnnotations() {
        if (isTextIndex()) {
            if (annotations != null && newAnnotations == null) {
                initTransientState();
            }
            return Collections.unmodifiableMap(newAnnotations);
        }
        return Collections.emptyMap();
    }

    Map getAnnotationsInternal() {
        if (annotations != null && newAnnotations == null) {
            initTransientState();
        }
        return newAnnotations;
    }

    public Map getProperties() {
        if (properties != null) {
            return properties;
        }
        return Collections.emptyMap();
    }

    @Override
    public String getDescription()  {
        return description;
    }

    @Override
    public IndexKeyImpl createIndexKey() {
        return new IndexKeyImpl(this, getIndexKeyDef());
    }

    /*
     * Creates an IndexKeyImpl from a RecordValue that is known to be
     * a flattened IndexKey. This is used by the query engine where IndexKeys
     * are serialized and deserialized as plain RecordValue instances.
     *
     * Unlike the method below, this assumes a flattened structure in value
     * ("value" must have the same type def as the IndexKey).
     */
    public IndexKeyImpl createIndexKeyFromFlattenedRecord(RecordValue value) {
        IndexKeyImpl ikey = createIndexKey();
        ikey.copyFrom(value);
        return ikey;
    }

    @Override
    public IndexKeyImpl createIndexKey(RecordValue value) {
        if (value instanceof IndexKey) {
            throw new IllegalArgumentException(
                "Cannot call createIndexKey with IndexKey argument");
        }
        IndexKeyImpl ikey = createIndexKey();
        populateIndexRecord(ikey, (RecordValueImpl) value);
        return ikey;
    }

    @Override
    public IndexKey createIndexKeyFromJson(String jsonInput, boolean exact) {
        return createIndexKeyFromJson
            (new ByteArrayInputStream(jsonInput.getBytes()), exact);
    }

    @Override
    public IndexKey createIndexKeyFromJson(InputStream jsonInput,
                                           boolean exact) {
        IndexKeyImpl key = createIndexKey();

        /*
         * Using addMissingFields false to not add missing fields, if Json
         * contains a subset of index fields, then build partial index key.
         */
        ComplexValueImpl.createFromJson(key, jsonInput, exact,
                                        false /*addMissingFields*/);
        return key;
    }

    @Override
    public FieldRange createFieldRange(String path) {

        FieldDef ifieldDef = getIndexKeyDef().getField(path);

        if (ifieldDef == null) {
            throw new IllegalArgumentException(
                "Field does not exist in index: " + path);
        }
        return new FieldRange(path, ifieldDef, 0);
    }

    /**
     * Populates the IndexKey from the record, handling complex values.
     */
    private void populateIndexRecord(IndexKeyImpl indexKey,
                                     RecordValueImpl value) {
        assert !(value instanceof IndexKey);
        int i = 0;
        for (IndexField field : getIndexFields()) {
            FieldValueImpl v = value.findFieldValue(field, -1, null);
            if (v != null) {
                indexKey.put(i, v);
            }
            i++;
        }
        indexKey.validate();
    }

    public int numFields() {
        return getFields().size();
    }

    /**
     * Returns true if the index comprises only fields from the table's primary
     * key.  Nested types can't be key components so there is no need to handle
     * a complex path.
     */
    public boolean isKeyOnly() {

        for (IndexField ifield : getIndexFields()) {
            if (ifield.isComplex()) {
                return false;
            }
            if (!table.isKeyComponent(ifield.getStep(0))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Return true if this index has multiple keys per record.  This can happen
     * if there is an array or map in the index.  An index can only contain one
     * array or map.
     */
    public boolean isMultiKey() {

    	if (!isTextIndex()) {
            for (IndexField field : getIndexFields()) {
                if (field.isMultiKey()) {
                    return true;
                }
            }
    	}
      return false;
    }

    public IndexStatus getStatus() {
        return status;
    }

    public void setStatus(IndexStatus status) {
        this.status = status;
    }

    public List getTypes() {
        return types;
    }

    /**
     * Returns the list of IndexField objects defining the index. It is
     * transient, and if not yet initialized, initialize it.
     */
    public List getIndexFields() {
        if (indexFields == null) {
            initTransientState();
        }
        return indexFields;
    }

    public RecordDefImpl getIndexKeyDef() {
        if (indexKeyDef == null) {
            initTransientState();
        }
        return indexKeyDef;
    }

    public RecordDefImpl getIndexEntryDef() {
        if (indexEntryDef == null) {
            initTransientState();
        }
        return indexEntryDef;
    }

    public String getFieldName(int i) {
        return getIndexKeyDef().getFieldName(i);
    }

    public FieldDefImpl getFieldDef(int i) {
        return getIndexKeyDef().getFieldDef(i);
    }

    /**
     * Compares if the specified index fields are equals to that of this index,
     * return true if they are equal, otherwise return false.
     */
    public boolean compareIndexFields(List fieldNames) {

        if (fieldNames == null || fieldNames.size() != getFields().size()) {
            return false;
        }

        for (int i = 0; i < fieldNames.size(); i++) {
            String field = fieldNames.get(i);
            IndexField ifield = new IndexField(table, field,
                                               getFieldType(i), i);
            validateIndexField(ifield, true);
            if (!ifield.equals(getIndexFields().get(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Initializes the transient list of index fields.  This is used when
     * the IndexImpl was constructed via deserialization and the constructor
     * and validate() were not called.
     *
     * TODO: figure out how to do transient initialization in the
     * deserialization case.  It is not as simple as implementing readObject()
     * because an intact Table is required.  Calling validate() from TableImpl's
     * readObject() does not work either (generates an NPE).
     */
    private synchronized void initTransientState() {

        /*
         * Since the indexKeyDef is the last thing to initialize in this
         * method, so check this depends on the setting of indexKeyDef.
         */
        if (indexKeyDef != null) {
            return;
        }

        geoFieldPos = -1;

        int numFields;
        boolean convertOldPathFormat = false;

        if (newFields == null) {
            convertOldPathFormat = true;
            numFields = fields.size();
            newFields = new ArrayList(numFields);

            if (annotations != null) {
                newAnnotations = new HashMap();
            }
        } else {
            numFields = newFields.size();
        }

        indexFields = new ArrayList(numFields);

        for (int i = 0; i < numFields; ++i) {

            String newField;

            if (convertOldPathFormat) {
                String field = fields.get(i);
                newField = convertOldIndexPath(field);
            } else {
                newField = newFields.get(i);
            }

            IndexField indexField = new IndexField(table, newField,
                                                   getFieldType(i), i);

            if (indexField.isPoint()) {
                isPointIndex = true;
                geoFieldPos = i;
            }  else if (indexField.isGeometry()) {
                geoFieldPos = i;
            }

            try {
                validateIndexField(indexField, false);
            } catch (RuntimeException ex) {
                if (convertOldPathFormat) {
                    newFields = null;
                    if (newAnnotations != null) {
                        newAnnotations = null;
                    }
                }
                throw ex;
            }

            if (convertOldPathFormat) {
                newFields.add(newField);

                if (annotations != null) {
                    String ann = annotations.get(fields.get(i));
                    if (ann != null) {
                        newAnnotations.put(newField, ann);
                    }
                }
            }

            indexFields.add(indexField);
        }

        indexKeyDef = createIndexKeyDef();
        indexEntryDef = createIndexEntryDef();
    }

    /*
     * The field path may be in the old format, that uses ._key and .[] steps.
     * If so, it must be converted to the new format.
     *
     * ._key steps are easy: just replace them with .keys().
     *
     * .[] steps may apply to either arrays or maps, and must be converted to
     * [] or .values(), respectively. To do so, we have to know the type of
     * the path up to the .[] step.
     */
    private String convertOldIndexPath(String field) {

        if (field.contains(KEY_TAG)) {
            return field.replace(KEY_TAG, TableImpl.KEYS);
        }

        if (field.contains(DOT_BRACKETS)) {

            int bracketsIdx = field.indexOf(DOT_BRACKETS);

            String mapOrArrayPath = field.substring(0, bracketsIdx);

            /*
             * mapOrArrayPath should contain only dot-separated identifiers,
             * so it is ok to call table.findTableField() on it.
             */
            FieldDefImpl def = table.findTableField(mapOrArrayPath);

            if (def.isArray()) {
                return field.replace(DOT_BRACKETS, TableImpl.BRACKETS);
            }

            if (!def.isMap()) {
                /*
                 * Maybe it's a json index that is being deserialized at an
                 * old client.
                 */
                throw new IllegalArgumentException(
                    "Multikey index path does not contain an array or map. " +
                    "mapOrArrayPath = " + mapOrArrayPath);
            }

            return field.replace(TableImpl.BRACKETS, TableImpl.VALUES);
        }

        /*
         * The path contains dot-separated identifiers only. However, it may
         * still lead to or cross an array. This is possible if the index was
         * created with a KVS version prior to 4.2, because at that itme []
         * was optional for arrays. In this case, we MUST find the array step
         * and add the [] after it. This will be done in validateIndexField().
         */
        return field;
    }

    /**
     * If there's a multi-key field in the index return a new IndexField
     * based on the the path to the complex instance.
     */
    private IndexField findMultiKeyField() {

        for (IndexField field : getIndexFields()) {
            if (field.isMultiKey()) {
                return field.getMultiKeyField();
            }
        }

        throw new IllegalStateException
            ("Could not find any multiKeyField in index " + name);
    }

    public boolean isMultiKeyMapIndex() {
        return isMultiKeyMapIndex;
    }

    public boolean isGeoIndex() {
        return isGeoPointIndex() || isGeometryIndex();
    }

    public boolean isGeoPointIndex() {
        initTransientState();
        return isPointIndex;
    }

    public boolean isGeometryIndex() {
        initTransientState();
        return geoFieldPos >= 0 && !isPointIndex;
    }

    public int getGeoFieldPos() {
        initTransientState();
        return geoFieldPos;
    }

    int geoMaxCoveringCells() {

        if (properties != null) {
            String val = properties.get("max_covering_cells");
            if (val != null) {
                Integer.valueOf(val);
            }
        }

        return GeometryUtils.theMaxCoveringCellsForIndex;
    }

    int geoMinCoveringCells() {

        if (properties != null) {
            String val = properties.get("min_covering_cells");
            if (val != null) {
                Integer.valueOf(val);
            }
        }

        return GeometryUtils.theMinCoveringCellsForIndex;
    }

    int geoMaxSplits() {

        if (properties != null) {
            String val = properties.get("max_splits");
            if (val != null) {
                Integer.valueOf(val);
            }
        }

        return GeometryUtils.theMaxSplits;
    }

    double geoSplitRatio() {

        if (properties != null) {
            String val = properties.get("split_ratio");
            if (val != null) {
                Double.valueOf(val);
            }
        }

        return GeometryUtils.theSplitRatio;
    }

    /**
     * If an index path of this index contains a .keys() step, return the
     * position of associated index field. Otherwise return -1. It is used
     * in query/compiler/IndexAnalyzer.java
     */
    public int getPosForKeysField() {

        for (IndexField field : getIndexFields()) {
            if (field.isMapKeys()) {
                return field.getPosition();
            }
        }

        return -1;
    }

    /**
     * Extracts an index key from the key and data for this
     * index.  The key has already matched this index.
     *
     * @param key the key bytes
     *
     * @param data the row's data bytes
     *
     * @param keyOnly true if the index only uses key fields.  This
     * optimizes deserialization.
     *
     * @return the byte[] serialization of an index key or null if there
     * is no entry associated with the row, or the row does not match a
     * table record.
     *
     * While not likely it is possible that the record is not actually  a
     * table record and the key pattern happens to match.  Such records
     * will fail to be deserialized and throw an exception.  Rather than
     * treating this as an error, silently ignore it.
     *
     * TODO: maybe make this faster.  Right now it turns the key and data
     * into a Row and extracts from that object which is a relatively
     * expensive operation, including full Avro deserialization.
     */
    public byte[] extractIndexKey(byte[] key,
                                  byte[] data,
                                  boolean keyOnly) {
        RowImpl row = table.createRowFromBytes(key, data, keyOnly);
        if (row != null) {
            return serializeIndexKey(row, -1);
        }
        return null;
    }

    /**
     * Extracts multiple index keys from a single record.  This is used if
     * one of the indexed fields is an array.  Only one array is allowed
     * in an index.
     *
     * @param key the key bytes
     *
     * @param data the row's data bytes
     *
     * @param keyOnly true if the index only uses key fields.  This
     * optimizes deserialization.
     *
     * @return a List of byte[] serializations of index keys or null if there
     * is no entry associated with the row, or the row does not match a
     * table record.  This list may contain duplicate values.  The caller is
     * responsible for handling duplicates (and it does).
     *
     * While not likely it is possible that the record is not actually  a
     * table record and the key pattern happens to match.  Such records
     * will fail to be deserialized and throw an exception.  Rather than
     * treating this as an error, silently ignore it.
     *
     * TODO: can this be done without reserializing to Row?  It'd be
     * faster but more complex.
     *
     * 1.  Deserialize to RowImpl
     * 2.  Find the map or array value and get its size
     * 3.  for each map or array entry, serialize a key using that entry
     */
    public List extractIndexKeys(byte[] key,
                                         byte[] data,
                                         boolean keyOnly) {

        RowImpl row = table.createRowFromBytes(key, data, keyOnly);
        return extractIndexKeys(row);
    }

    public List extractIndexKeys(RowImpl row) {

        if (row == null) {
            return null;
        }

        ArrayList returnList;
        final FieldValueImpl empty = EmptyValueImpl.getInstance();

        IndexField arrayOrMapPath = (isGeometryIndex() ?
                                     getIndexPath(getGeoFieldPos()) :
                                     findMultiKeyField());

        /*
         * Look for the map/array that is the source of the multiple index
         * entries from the current row.
         */
        FieldValueImpl mapOrArrayVal = row.findFieldValue(arrayOrMapPath,
                                                          -1,/*arrayIndex*/
                                                          null/*mapkey*/);
        if (isGeometryIndex()) {

            if (mapOrArrayVal.isNull() || mapOrArrayVal.isEMPTY()) {
                byte[] serKey = serializeIndexKey(row, -1, mapOrArrayVal);
                returnList = new ArrayList(1);
                if (serKey != null) {
                    returnList.add(serKey);
                }
            } else {
                List geoHashes = 
                    CompilerAPI.getGeoUtils().
                    hashGeometry(mapOrArrayVal,
                                 geoMaxCoveringCells(),
                                 geoMinCoveringCells(),
                                 geoMaxSplits(),
                                 geoSplitRatio());

                returnList = new ArrayList(geoHashes.size());

                for (String hash : geoHashes) {

                    byte[] serKey = serializeIndexKey(row, -1, null, hash);
                    /*
                    System.out.println("Added geohash " + hash +
                                       " to index " + getName() +
                                       " for geometry " + mapOrArrayVal +
                                       " index key = " +
                                       PlanIter.printByteArray(serKey));
                    */
                    if (serKey != null) {
                        returnList.add(serKey);
                    }
                }
            }

        } else if (isMultiKeyMapIndex) {

            if (mapOrArrayVal.isMap()) {
                MapValueImpl map = (MapValueImpl)mapOrArrayVal;

                if (map.size() == 0) {
                    byte[] serKey = serializeIndexKey(row, -1, empty);
                    returnList = new ArrayList(1);
                    if (serKey != null) {
                        returnList.add(serKey);
                    }
                } else {
                    returnList = new ArrayList(map.size());

                    for (String mapKey : map.getFieldsInternal().keySet()) {
                        byte[] serKey = serializeIndexKey(row, mapKey);
                        if (serKey != null) {
                            returnList.add(serKey);
                        }
                    }
                }
            } else if (mapOrArrayVal.isNull() || mapOrArrayVal.isAtomic()) {

                /*
                 * If the map is NULL, we put NULL in the index entry,
                 * If the map is atomic (which includes json null or EMPTY),
                 * we put EMPTY.
                 */
                if (!mapOrArrayVal.isNull()) {
                    mapOrArrayVal = empty;
                }

                byte[] serKey = serializeIndexKey(row, -1, mapOrArrayVal);
                returnList = new ArrayList(1);
                if (serKey != null) {
                    returnList.add(serKey);
                }
            } else {
                throw new IllegalArgumentException(
                    "Cannot create index entry for index " + getName() +
                    " on row\n" + row + "\nThe row does not contain a map " +
                    "at path " + findMultiKeyField());
            }

        } else {
            if (mapOrArrayVal.isArray()) {
                ArrayValueImpl array = (ArrayValueImpl)mapOrArrayVal;

                if (array.size() == 0) {
                    byte[] serKey = serializeIndexKey(row, -1, empty);
                    returnList = new ArrayList(1);
                    if (serKey != null) {
                        returnList.add(serKey);
                    }
                } else {
                    int size = array.size();

                    returnList = new ArrayList(size);

                    for (int i = 0; i < size; i++) {
                        byte[] serKey = serializeIndexKey(row, i, null);
                        if (serKey != null) {
                            returnList.add(serKey);
                        }
                    }
                }
            } else {
                /*
                 * If mapOrArrayVal is not an array, there should not be any
                 * array at all in the path. We call serializeIndexKey() passing
                 * -1 as the array index to make sure that this is the case.
                 * serializeIndexKey() will do the right thing in all cases.
                 * Specifically:
                 * - If mapOrArrayVal is NULL, it will put NULL in the index.
                 * - If mapOrArrayVal is EMPTY, it will put EMPTY in the index.
                 * - Else, the index path should be evaluated as if
                 *   mapOrArrayVal was a single-element array containing the
                 *   mapOrArrayVal. In this case what gets into the index
                 *   depends on the kind of mapOrArrayVal and where the [] are
                 *   in the index path. For example, if mapOrArrayVal is an
                 *   atomic value, the if the [] is the last step of the index
                 *   path, mapOrArrayVal is put into the index (if it belongs
                 *   to the declared type of the path); otherwise EMPTY is put
                 *   into the index.
                 */
                returnList = new ArrayList(1);

                byte[] serKey = serializeIndexKey(row, -1, null);
                if (serKey != null) {
                    returnList.add(serKey);
                }
            }
        }

        return returnList;
    }

    /*
     * Generates a JSON ObjectNode representing the index. The input side
     * of this representation is handled in JsonTableUtils.indexFromJsonNode().
     */
    public void toJsonNode(ObjectNode node) {

        node.put(NAME, name);
        if (table.getInternalNamespace() != null) {
            node.put(NAMESPACE, table.getInternalNamespace());
        }

        node.put(TABLE, table.getFullName());

        node.put(TYPE, getType().toString().toLowerCase());

        if (description != null) {
            node.put(DESC, description);
        }

        if (isMultiKey()) {
            node.put("multi_key", "true");
        }

        ArrayNode fieldArray = node.putArray(FIELDS);
        for (String field : getFields()) {
            fieldArray.add(field);
        }

        if (types != null && types.size() != 0) {
            ArrayNode typesArray = node.putArray(TYPES);
            for (FieldDef.Type type : types) {
                if (type == null) {
                    typesArray.addNull();
                } else {
                    typesArray.add(type.toString());
                }
            }
        }

        if (annotations != null) {
            putMapAsJson(node, "annotations", getAnnotationsInternal());
        }
        if (properties != null) {
            putMapAsJson(node, "properties", properties);
        }
    }

    private static void putMapAsJson(ObjectNode node,
                                     String mapName,
                                     Map map) {
        ObjectNode mapNode = JsonNodeFactory.instance.objectNode();
        for (Map.Entry entry : map.entrySet()) {
            mapNode.put(entry.getKey(), entry.getValue());
        }
        node.put(mapName, mapNode);
    }

    FieldDef.Type getFieldType(int position) {
        if (types == null) {
            return null;
        }
        return types.get(position);
    }

    /**
     * Validate that the name, fields, and types of the index match
     * the table.  This also initializes the (transient) list of index fields in
     * indexFields, so that member must not be used in validate() itself.
     *
     * This method must only be called from the constructor.  It is not
     * synchronized and changes internal state.
     */
    private void validate() {

        TableImpl.validateIdentifier(name,
                                     TableImpl.MAX_NAME_LENGTH,
                                     "Index names");

        IndexField multiKeyField = null;

        if (getFields().isEmpty()) {
            throw new IllegalCommandException(
                "Index requires at least one field");
        }

        assert indexFields == null;

        indexFields = new ArrayList(getFields().size());

        int position = 0;
        for (String field : getFields()) {

            if (field == null || field.length() == 0) {
                throw new IllegalCommandException(
                    "Invalid (null or empty) index field name");
            }

            IndexField ifield = new IndexField(table, field,
                                               getFieldType(position),
                                               position++);

            if ((ifield.isPoint() || ifield.isGeometry()) && geoFieldPos >= 0) {
                throw new IllegalCommandException(
                    "An index cannot index more than one " +
                    "geometry/point fields");
            }

            if (ifield.isPoint()) {
                isPointIndex = true;
                geoFieldPos = ifield.getPosition();

            } else if (ifield.isGeometry()) {

                if (multiKeyField != null) {
                    throw new IllegalCommandException(
                        "An index cannot index both a geometry field and " +
                        "an array or map field");
                }

                geoFieldPos = ifield.getPosition();
            }

            /*
             * The check for multiKey needs to consider all fields as well as
             * fields that reference into complex types.  A multiKey field may
             * occur at any point in the navigation path (first, interior, leaf).
             *
             * The call to isMultiKey() will set the multiKey state in
             * the IndexField.
             *
             * Allow more than one multiKey field in a single index IFF they are
             * in the same object (map or array).
             */
            validateIndexField(ifield, true);

            if (ifield.isMultiKey() &&
                geoFieldPos >= 0 &&
                (ifield.isGeometry() || geoFieldPos != ifield.getPosition())) {
                throw new IllegalCommandException(
                    "An index cannot index both a geometry field and " +
                    "an array or map field");
            }

            /* Don't restrict number of multi-key fields for text indexes. */
            if (ifield.isMultiKey() && !isTextIndex()) {

                IndexField mkey = ifield.getMultiKeyField();

                if (multiKeyField != null && !mkey.equals(multiKeyField)) {
                    throw new IllegalCommandException(
                        "Indexes may index only one array or only one map (" +
                        "this implies that all multi-key paths in an index " +
                        "definition must use the same path before their " +
                        "multi-key step)");
                }
                multiKeyField = mkey;
            }

            if (indexFields.contains(ifield)) {
                throw new IllegalCommandException(
                    "Index already contains the field: " + field);
            }

            indexFields.add(ifield);
        }

        assert newFields.size() == indexFields.size();

        /*
         * initialize transient RecordDef representing the IndexKeyImpl
         * definition.
         */
        indexKeyDef = createIndexKeyDef();
        indexEntryDef = createIndexEntryDef();

        table.checkForDuplicateIndex(this);
    }

    /**
     * Validates the given index path expression (ipath) and returns its data
     * type (which must be one of the indexable atomic types).
     *
     * This call has a side effect of setting the multiKey state in the
     * IndexField so that the lookup need not be done twice.
     */
    private void validateIndexField(IndexField ipath, boolean isNewIndex) {

        int numSteps = ipath.numSteps();

        int stepIdx = 0;
        String step = ipath.getStep(stepIdx);
        FieldDef stepDef = ipath.getFirstDef();

        if (stepDef == null) {
            throw new IllegalCommandException(
                "Invalid index field definition : " + ipath + "\n" +
                "There is no field named " + step);
        }

        while (stepIdx < numSteps) {

            /*
             * TODO: Prevent any path through these types from
             * participating in a text index, until the text index
             * implementation supports them correctly.
             */
            if (isTextIndex() &&
                (stepDef.isBinary() ||
                 stepDef.isFixedBinary() ||
                 stepDef.isEnum())) {
                    throw new IllegalCommandException(
                        "Invalid index field definition : " + ipath + "\n" +
                        "Fields of type " + stepDef.getType() +
                        " cannot participate in a FULLTEXT index.");
            }

            if (stepDef.isJson()) {

                ipath.setIsJson();
                stepIdx++;

                if (ipath.getJsonFieldPath() == null) {
                    ipath.setJsonFieldPath(stepIdx);
                }

                if (stepIdx >= numSteps) {
                    break;
                }

                if (ipath.isBracketsStep(stepIdx) ||
                    ipath.isKeysStep(stepIdx) ||
                    ipath.isValuesStep(stepIdx)) {

                    ipath.setIsJsonMultiKey();
                    ipath.setMultiKeyPath(stepIdx);

                    if (ipath.isKeysStep(stepIdx)) {
                        ipath.setIsMapKeys();
                        ipath.declaredType = FieldDefImpl.stringDef;
                        isMultiKeyMapIndex = true;
                    } else if (ipath.isValuesStep(stepIdx)){
                        ipath.setIsMapValues();
                        isMultiKeyMapIndex = true;
                    }
                } else {
                    ipath.setIsMapKeyStep(stepIdx);
                }

            } else if (stepDef.isRecord()) {

                ++stepIdx;
                if (stepIdx >= numSteps) {
                    break;
                }

                step = ipath.getStep(stepIdx);
                stepDef = stepDef.asRecord().getFieldDef(step);

                if (stepDef == null) {
                    throw new IllegalCommandException(
                        "Invalid index field definition : " + ipath + "\n" +
                        "There is no field named \"" + step + "\" after " +
                        "path " + ipath.getPathName(stepIdx - 1));
                }

            } else if (stepDef.isArray()) {

                if (ipath.isMultiKey()) {
                    throw new IllegalCommandException(
                        "Invalid index field definition : " + ipath + "\n" +
                        "The definition contains more than one multi-key " +
                        "fields. The second multi-key field is " + step);
                }

                /*
                 * If there is a next step and it is [], consume it.
                 *
                 * Else, if we are creating a new index, throw an exception,
                 * because as of version 4.2 the use of [] is mandatory for
                 * array indexes.
                 *
                 * Otherwise, we must be reading from a store created prior
                 * tp v4.2. In this case, the [] may be missing. We must add
                 * it to the index path, because it is expected to be there
                 * in other parts of the code (for example, the index matching
                 * done by query processor).
                 */
                if (stepIdx + 1 < numSteps &&
                    ipath.getStep(stepIdx + 1).equals(TableImpl.BRACKETS)) {
                    ++stepIdx;

                } else if (isNewIndex) {
                    throw new IllegalCommandException(
                        "Invalid index field definition : " + ipath + "\n" +
                        "Can not index an array as a whole; use " + step +
                        "[]  to index the elements of the array");

                } else {
                    ++stepIdx;
                    ++numSteps;
                    ipath.add(stepIdx, TableImpl.BRACKETS, false);
                }

                ipath.setMultiKeyPath(stepIdx);

                step = TableImpl.BRACKETS;
                stepDef = stepDef.asArray().getElement();

            } else if (stepDef.isMap()) {

                ++stepIdx;
                if (stepIdx >= numSteps) {
                    throw new IllegalCommandException(
                        "Invalid index field definition : " + ipath + "\n" +
                        "Can not index a map as a whole; use " +
                        ".values() to index the elements of the map or " +
                        ".keys() to index the keys of the map");
                }

                step = ipath.getStep(stepIdx);

                if (ipath.isValuesStep(stepIdx) ||
                    ipath.isBracketsStep(stepIdx)) {

                    if (ipath.isMultiKey()) {
                        throw new IllegalCommandException(
                            "Invalid index field definition : " + ipath + "\n" +
                            "The definition contains more than one multi-key " +
                            "fields. The second multi-key field is " + step);
                    }

                    if (ipath.isBracketsStep(stepIdx)) {
                        ipath.setIsValuesStep(stepIdx);
                    }

                    ipath.setMultiKeyPath(stepIdx);
                    ipath.setIsMapValues();
                    isMultiKeyMapIndex = true;

                    /* Consume the .values() step */
                    stepDef = stepDef.asMap().getElement();

                } else if (ipath.isKeysStep(stepIdx)) {

                    if (ipath.isMultiKey()) {
                        throw new IllegalCommandException(
                            "Invalid index field definition : " + ipath + "\n" +
                            "The definition contains more than one multi-key " +
                            "fields. The second multi-key field is " + step);
                    }

                    ipath.setMultiKeyPath(stepIdx);
                    ipath.setIsMapKeys();
                    isMultiKeyMapIndex = true;

                    /* Consume the .keys() step */
                    stepDef = FieldDefImpl.stringDef;

                } else {
                    ipath.setIsMapKeyStep(stepIdx);
                    stepDef = stepDef.asMap().getElement();
                }

            } else {

                ++stepIdx;
                if (stepIdx >= numSteps) {
                    break;
                }

                step = ipath.getStep(stepIdx);
                throw new IllegalCommandException(
                    "Invalid index field definition : " + ipath + "\n" +
                    "There is no field named \"" + step + "\" after " +
                    "path " + ipath.getPathName(stepIdx - 1));
            }
        }

        if (!stepDef.isValidIndexField()) {
            throw new IllegalCommandException(
                "Invalid index field definition : " + ipath + "\n" +
                "Cannot index values of type " + stepDef);
        }

        /*
         * If NULLs are allowed in index key, the nullablity of 2 kinds of
         * field below is true:
         *  1. The complex field or nested field of complex type.
         *  2. The simple field is nullable and not a primary key field.
         */
        boolean nullable = (ipath.isComplex() ||
                            (!table.isKeyComponent(ipath.getStep(0)) &&
                             ipath.getFieldMap()
                                 .getFieldMapEntry(ipath.getStep(0))
                                     .isNullable())) &&
                           this.supportsSpecialValues();
        ipath.setNullable(nullable);

        /*
         * Specific types in index declarations are only allowed for JSON, and
         * in this path, the IndexField's typeDef becomes that of the specified
         * type, and not JSON.
         */
        if (ipath.getDeclaredType() != null) {

            if (!stepDef.isJson()) {
                throw new IllegalCommandException(
                    "Invalid index field definition: " + ipath + "\n" +
                    "Specific types are only allowed for JSON data types.");
            }

            ipath.setType(ipath.getDeclaredType());
            return;
        }

        if (stepDef.isJson()) {
            throw new IllegalCommandException(
                "Invalid index field definition: " + ipath + "\n" +
                "Please specify data type for JSON index field.");
        }

        ipath.type = (FieldDefImpl) stepDef;
    }

    @Override
    public String toString() {
        return "Index[" + name + ", " + table.getId() + ", " + status + "]";
    }

    /**
     * Creates a binary index key from an IndexKey. In this case
     * the IndexKey may be partially filled.
     *
     * This is the version used by most client-based callers.
     *
     * @return the serialized index key or null if the IndexKey cannot
     * be serialized (e.g. it has null values).
     */
    public byte[] serializeIndexKey(IndexKeyImpl indexKey) {
        return serializeIndexKey(indexKey, SerialVersion.CURRENT);
    }

    private byte[] serializeIndexKey(IndexKeyImpl indexKey,
                                     short opSerialVersion) {
        TupleOutput out = null;

        try {
            out = new TupleOutput();

            int numFields = getIndexKeyDef().getNumFields();

            boolean keyHasIndicators = (opSerialVersion >= SerialVersion.V12);

            for (int i = 0; i < numFields; ++i) {

                FieldValueImpl val = indexKey.get(i);

                if (val == null) {
                    /* A partial key, done with fields */
                    break;
                }

                if (FieldValueImpl.isSpecialValue(val) &&
                    (!this.supportsSpecialValues() || !keyHasIndicators)) {
                    return null;
                }

                serializeValue(out, val, indexFields.get(i),
                               false, /*fromRow*/
                               opSerialVersion);
            }

            return (out.size() != 0 ? out.toByteArray() : null);

        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (IOException ioe) {
            }
        }
    }

    /**
     * Create a binary index key from a row. This method is used for simple
     * (non-multi-key) indexes and for indexes on arrays. It is also used
     * for multi-key map indexes if the map is null or empty inside the given
     * row.
     *
     * @param record the record to extract a binary index key from. Actually,
     * this is RowImpl. The caller can vouch for the validity of the object.
     *
     * @param arrayIndex will be 0 if not doing an array lookup, or if the
     * desired array index is actually 0.  For known array lookups it may be
     * >0.
     *
     * @return the serialized index key or null if the record cannot
     * be serialized.
     *
     * TODO: consider sharing more code with the other serializeIndexKey()
     * method.
     *
     * This is public so it can be used by TableSizeCommand. Otherwise it'd be
     * private.
     */
    public byte[] serializeIndexKey(RecordValueImpl record, int arrayIndex) {
        return serializeIndexKey(record, arrayIndex, null);
    }

    private byte[] serializeIndexKey(RecordValueImpl record,
                                     int arrayIndex,
                                     FieldValueImpl nullOrEmptyMapArray) {

        return serializeIndexKey(record, arrayIndex, nullOrEmptyMapArray, null);
    }

    private byte[] serializeIndexKey(RecordValueImpl record,
                                     int arrayIndex,
                                     FieldValueImpl nullOrEmptyMapArray,
                                     String geohash) {

        if (nullOrEmptyMapArray != null && !this.supportsSpecialValues()) {
            return null;
        }

        if (isMultiKeyMapIndex()) {
            if (nullOrEmptyMapArray == null) {
                throw new IllegalStateException("Wrong serializer for " +
                    "map index");
            }
        }

        TupleOutput out = null;

        try {
            out = new TupleOutput();

            for (IndexField field : getIndexFields()) {

                if (field.isGeometry()) {
                    out.writeByte(NORMAL_VALUE_INDICATOR);
                    out.writeString(geohash);
                } else {
                    FieldValueImpl val =
                        ((field.isMultiKey() && nullOrEmptyMapArray != null) ?
                         nullOrEmptyMapArray :
                         record.findFieldValue(field, arrayIndex, null));

                    /*
                     * In the case of a JSON index it is possible to resolve to
                     * a map or array (JSON object, JSON array). For now, if this
                     * happens, serialize a null (SQL).
                     *
                     * NOTE: in the near future it is likely that if a JSON array is
                     * encountered it will be indexed as a multi-value index, unless
                     * otherwise specified. JSON objects found as targets for
                     * indexing are not valid.
                     *
                     * Issue: JSON indexes may be multi-key and may not be. This
                     * will affect IndexField and how it handles multi-key indexes.
                     * At this time that state is static and not dependent on the
                     * actual data. Generic JSON indexes will always be potentially
                     * multi-key.
                     */

                    /*
                     * If the value is NULL or EMPTY, and the index does not
                     * support indexing of NULL/EMPTY, it is not possible to
                     * create a binary index key. In this case, this row has
                     * no entry for this index.
                     */
                    if (FieldValueImpl.isSpecialValue(val) &&
                        !this.supportsSpecialValues()) {
                        return null;
                    }

                    serializeValue(out, val, field);
                }
            }

            return (out.size() != 0 ? out.toByteArray() : null);

        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (IOException ioe) {
            }
        }
    }

    /**
     * Create a binary index key from a row. This method is used for multi-key
     * map indexes.
     *
     * @param record the record to extract a binary index key from. Actually,
     * this is RowImpl. The caller can vouch for the validity of the object.
     *
     * @param mapKey will be null if not doing a map lookup.
     *
     * @return the serialized index key or null if the record cannot
     * be serialized.
     *
     * These are conditions that will cause serialization to fail:
     * 1. The record has a null value in one of the index keys
     * 2. An index key field contains a map and the record does not
     *    have a value for the indexed map key value
     *
     * TODO: consider sharing more code with the other serializeIndexKey()
     * method.
     *
     * This method is package protected vs private because it's used by test
     * code.
     */
    byte[] serializeIndexKey(RecordValueImpl record, String key) {

        assert(isMultiKeyMapIndex());

        TupleOutput out = null;

        try {
            out = new TupleOutput();

            for (IndexField field : getIndexFields()) {

                FieldValueImpl val = record.findFieldValue(field, -1, key);

                /*
                 * If the value is NULL or EMPTY, and the index does not
                 * support indexing of NULL/EMPTY, it is not possible to
                 * create a binary index key. In this case, this row has
                 * no entry for this index.
                 */
                if (FieldValueImpl.isSpecialValue(val) &&
                    !this.supportsSpecialValues()) {
                    return null;
                }

                serializeValue(out, val, field);
            }

            return (out.size() != 0 ? out.toByteArray() : null);

        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (IOException ioe) {
            }
        }
    }

    /**
     * Serializes a specific scalar FieldValue that is a component of an index.
     */
    private void serializeValue(
        TupleOutput out,
        FieldValueImpl val,
        IndexField indexField) {

        serializeValue(out, val, indexField, true, SerialVersion.CURRENT);
    }

    private void serializeValue(
        TupleOutput out,
        FieldValueImpl val,
        IndexField indexField,
        boolean fromRow,
        short opSerialVersion) {

        /*
         * Handle various null forms and field separator
         */
        if (indexField.isNullable() && opSerialVersion >= SerialVersion.V12) {
            byte indicator = getSerializeIndicator(val, opSerialVersion);
            out.writeByte(indicator);
            if (indicator != NORMAL_VALUE_INDICATOR) {
                return;
            }
        }
        
        if (fromRow && indexField.isPoint()) {

            String geohash = CompilerAPI.getGeoUtils().hashPoint(val);

            /*
            System.out.println("Added geohash " + geohash +
                               " to index " + getName() +
                               " for geometry " + val);
            */
            out.writeString(geohash);

            return;
        }

        if (indexField.isJson()) {

            /*
             * Validate the type of the field if the index is defined with a
             * constrained type. This only works for exact matches of specific
             * scalar types. Testing for a scalar vs array index field, when
             * supported, must be done in callers.
             */
            FieldDefImpl declaredType = indexField.getDeclaredType();

            if (declaredType != null && declaredType.isPrecise()) {

                /*
                 * Check that the value belongs to a subtype of the
                 * declared type, and cast to the declared type if needed.
                 */
                FieldDefImpl valDef = val.getDefinition();
                FieldValueImpl castVal = null;
                if (valDef.isSubtype(declaredType)) {
                    castVal = (FieldValueImpl)val.castToSuperType(declaredType);
                }

                if (castVal != null) {
                    val = castVal;
                } else {
                    /* castVal == null means conversion above failed */
                    throw new IllegalArgumentException(
                        "Invalid type for JSON index field: " +
                        indexField + ". Type is " +
                        val.getType() + ", expected type is " +
                        declaredType.getType() + "\nvalue = " + val);
                }
            } else {
                /*
                 * Multiple types may be mapped to a single index type to
                 * simplify searches. E.g. generic JSON indexes may map
                 * all numeric types to Number in an index.
                 */
                val = convertToJsonValue(val);
                out.writeByte(val.getType().ordinal());
            }
        }

        switch (val.getType()) {
        case INTEGER:
            out.writeSortedPackedInt(val.asInteger().get());
            break;
        case STRING:
            out.writeString(val.asString().get());
            break;
        case LONG:
            out.writeSortedPackedLong(val.asLong().get());
            break;
        case DOUBLE:
            out.writeSortedDouble(val.asDouble().get());
            break;
        case FLOAT:
            out.writeSortedFloat(val.asFloat().get());
            break;
        case NUMBER:
            out.write(((NumberValueImpl)val).getBytes());
            break;
        case ENUM:
            /* enumerations are sorted by declaration order */
            out.writeSortedPackedInt(val.asEnum().getIndex());
            break;
        case BOOLEAN:
            out.writeBoolean(val.asBoolean().get());
            break;
        case TIMESTAMP:
            out.write(((TimestampValueImpl)val).getBytes(true));
            break;
        default:
            throw new IllegalStateException(
                "Type not supported in indexes: " + val.getType());
        }
    }

    public boolean supportsSpecialValues() {
        return isNullSupported;
    }

    private byte getSerializeIndicator(FieldValue val,
                                       short opSerialVersion) {
        if (val.isNull()) {
            return getNullIndicator(opSerialVersion);
        } else if (((FieldValueImpl)val).isEMPTY()) {
            return (opSerialVersion < SerialVersion.V14 ?
                    getNullIndicator(opSerialVersion) :
                    getEmptyIndicator());
        } else if (val.isJsonNull()) {
            return JSON_NULL_INDICATOR;
        }
        return NORMAL_VALUE_INDICATOR;
    }

    private FieldValueImpl getSpecialValue(byte indicator,
                                           short opSerialVersion) {
        return (indicator == getNullIndicator(opSerialVersion) ?
                NullValueImpl.getInstance() :
                (indicator == EMPTY_INDICATOR ?
                 EmptyValueImpl.getInstance() :
                 (indicator == JSON_NULL_INDICATOR ?
                  NullJsonValueImpl.getInstance() : null)));
    }

    private byte getNullIndicator(short opSerialVersion) {
        return (indexVersion >= INDEX_VERSION_V1 &&
                opSerialVersion >= SerialVersion.V14) ?
                NULL_INDICATOR_V1 : NULL_INDICATOR_V0;
    }

    private byte getEmptyIndicator() {
        return (indexVersion >= INDEX_VERSION_V1) ?
                EMPTY_INDICATOR : NULL_INDICATOR_V0;
    }

    /**
     * This method is used only to support queries compiled with clients older
     * than verion 18.1
     *
     * Deserialize the serialized format of an index key directly into a Row.
     *
     * Arrays -- if there is an array index the index key returned will
     * be the serialized value of a single array entry and not the array
     * itself. This value needs to be deserialized back into a single-value
     * array.
     *
     * Maps -- if there is a map index the index key returned will
     * be the serialized value of a single map entry.  It may be key-only or
     * it may be key + value. In both cases the map and the appropriate key
     * need to be created.
     *
     * In this path, partial population is allowed.
     *
     * @param data the bytes
     * @param row the Row to use.
     */
    public void rowFromIndexKey(byte[] data, RowImpl row) {

        TupleInput input = null;

        try {
            input = new TupleInput(data);

            /*
             * If the index indexes a .keys() path, keyForKeysField is the
             * value of this path in the current index entry. This is needed
             * by the putComplex method.
             */
            String keyForKeysField = null;

            for (IndexField ifield : getIndexFields()) {

                if (input.available() <= 0) {
                    break;
                }

                int jsonArrayPathPos = ifield.isJsonAsArray() ?
                        (ifield.getMultiKeyField().numSteps() - 1) : -1;

                if (ifield.mayHaveSpecialValue()) {
                    byte in = input.readByte();

                    FieldValueImpl specialVal =
                        getSpecialValue(in, SerialVersion.CURRENT);

                    if (specialVal != null) {
                        row.putComplex(ifield, specialVal, keyForKeysField,
                                       jsonArrayPathPos);
                        continue;
                    }
                }

                FieldDefImpl def = ifield.getType();
                FieldDef.Type type = (ifield.isJson() ?
                                      ifield.getDeclaredType().getType() :
                                      def.getType());

                switch (type) {
                case INTEGER:
                case STRING:
                case LONG:
                case DOUBLE:
                case BOOLEAN:
                case FLOAT:
                case NUMBER:
                case ENUM:
                case TIMESTAMP:
                    FieldValueImpl val = (FieldValueImpl)def.createValue(
                        type, FieldValueImpl.readTuple(type, def, input));

                    if (ifield.isMapKeys()) {
                       keyForKeysField = ((StringValueImpl)val).get();
                    }

                    row.putComplex(ifield, val, keyForKeysField,
                                   jsonArrayPathPos);
                    break;

                 default:
                    throw new IllegalStateException
                        ("Type not supported in indexes: " + type);
                }
            }
        } finally {
            try {
                if (input != null) {
                    input.close();
                }
            } catch (IOException ioe) {
                /* ignore IOE on close */
            }
        }
    }

    /**
     * Deserialize the binary format of an index entry into a (flat)
     * RecordValue that contains both the index key values and the
     * associated primary key values.
     */
    public void rowFromIndexEntry(RecordValueImpl row,
                                  byte[] primKey,
                                  byte[] indexKey) {

        TupleInput input = null;

        try {
            input = new TupleInput(indexKey);

            /*
             * Deserialize the index key (not including the associated
             * primary key).
             */
            for (int pos = 0; pos < numFields(); ++pos) {

                if (input.available() <= 0) {
                    throw new IllegalStateException(
                        "Index key desrialization error: the index key " +
                        "is too short");
                }

                IndexField ifield = getIndexPath(pos);

                if (ifield.mayHaveSpecialValue()) {
                    byte in = input.readByte();

                    FieldValueImpl specialVal =
                        getSpecialValue(in, SerialVersion.CURRENT);

                    if (specialVal != null) {
                        row.putInternal(pos, specialVal);
                        continue;
                    }
                }

                FieldDefImpl fdef = indexKeyDef.getFieldDef(pos);
                FieldDef.Type ftype = fdef.getType();

                switch (ftype) {
                case INTEGER:
                case STRING:
                case LONG:
                case DOUBLE:
                case BOOLEAN:
                case FLOAT:
                case NUMBER:
                case ENUM:
                case TIMESTAMP:
                    FieldValueImpl val = (FieldValueImpl)fdef.createValue(
                        ftype, FieldValueImpl.readTuple(ftype, fdef, input));
                    row.putInternal(pos, val);
                    break;

                 default:
                    throw new IllegalStateException(
                        "Type not supported in indexes: " + ftype);
                }
            }
        } finally {
            try {
                if (input != null) {
                    input.close();
                }
            } catch (IOException ioe) {
                /* ignore IOE on close */
            }
        }

        /*
         * Deserialize the values of the associated primary key.
         */
        if (primKey != null) {
            if (!table.initRowFromKeyBytes(primKey, numFields(), row)) {
                throw new IllegalStateException(
                    "Failed to deserialize primary key retrieved from index " +
                    getName());
            }
        }
    }

    public IndexKeyImpl deserializeIndexKey(byte[] data, boolean partialOK) {
        return deserializeIndexKey(data, partialOK, SerialVersion.CURRENT);
    }

    /**
     * Deserializes binary index keys into IndexKeyImpl. This method is used
     * both at the server and the client:
     *
     * (a) At the client it deserializes binary index keys that are sent
     * from the server during an IndexKeysIterate operation. These keys are
     * "full"index keys that have been extracted from an index at the server
     * partialOK is false in this case.
     *
     * (b) At the server it deserializes binary search index keys that are
     * sent from a client as the start/stop keys of an IndexOperation.
     * partialOK is true in this case.
     *
     * @param data the bytes
     * @param partialOK true if not all fields must be in the data stream.
     * @param opSerialVersion In case (b) above, this is the serial version
     *        used by the IndexOperation.
     * @return an instance of IndexKeyImpl
     */
    IndexKeyImpl deserializeIndexKey(byte[] data,
                                     boolean partialOK,
                                     short opSerialVersion) {
        TupleInput input = null;
        RecordDefImpl idxKeyDef = getIndexKeyDef();
        IndexKeyImpl indexKey = new IndexKeyImpl(this, idxKeyDef);

        /*
         * For versions >= 4.2, each index field includes an "indicator" byte.
         * For 4.2 and 4.3 the byte is a NULL indicator. For 4.4+ it is a
         * "special value" indicator (NULL, json null, or empty). Furthermore,
         * the value for the NULL indicator has changed between 4.3 and 4.4. In
         * case (b) above, we need to check if the client key contains indicators
         * and if so, map the value of each indicator to the correct special
         * value. In case (a) we don't need to do this, because the server
         * makes sure that the binary keys sent to the client have the format
         * that is expected by the client In this case, opSerialVersion will
         * be SerialVersion.CURRENT.
         */
        boolean keyHasIndicators = (opSerialVersion >= SerialVersion.V12);

        try {
            input = new TupleInput(data);

            int numFields = idxKeyDef.getNumFields();

            for (int i = 0; i < numFields; ++i) {

                if (input.available() <= 0) {
                    break;
                }

                IndexField ifield = getIndexPath(i);

                if (keyHasIndicators && ifield.mayHaveSpecialValue()) {

                    byte in = input.readByte();

                    FieldValueImpl specialVal =
                        getSpecialValue(in, opSerialVersion);

                    if (specialVal != null) {
                        indexKey.put(i, specialVal);
                        continue;
                    }
                }

                FieldDefImpl def = idxKeyDef.getFieldDef(i);
                FieldDef.Type type = def.getType();

                switch (type) {
                case INTEGER:
                case STRING:
                case LONG:
                case DOUBLE:
                case FLOAT:
                case NUMBER:
                case ENUM:
                case BOOLEAN:
                case TIMESTAMP:
                    FieldValue val = def.createValue(
                        type, FieldValueImpl.readTuple(type, def, input));
                    indexKey.put(i, val);
                    break;

                 default:
                     throw new IllegalStateException(
                         "Type not supported in indexes: " + type);
                }
            }

            if (!partialOK && !indexKey.isComplete()) {
                throw new IllegalStateException(
                    "Missing fields from index data for index " +
                    getName() + ", expected " + numFields +
                    ", received " +   indexKey.size());
            }
            return indexKey;
        } finally {
            try {
                if (input != null) {
                    input.close();
                }
            } catch (IOException ioe) {
                /* ignore IOE on close */
            }
        }
    }

    /*
     * See javadoc for IndexOperationHandler.reserializeOldKeys().
     */
    byte[] reserializeOldKey(byte[] key, short opVersion) {

        IndexKeyImpl ikey = deserializeIndexKey(key,
                                                true, /*partialOk*/
                                                opVersion);
        return serializeIndexKey(ikey);
    }

    /*
     * See javadoc for IndexKeysIterateHandler.reserializeToOldKeys()
     */
    public byte[] reserializeToOldKey(byte[] indexKey, short opVersion) {

        IndexKeyImpl ikey = deserializeIndexKey(indexKey,
                                                false, /* partialOK */
                                                SerialVersion.CURRENT);

        return serializeIndexKey(ikey, opVersion);
    }

    /**
     * Checks to see if the index contains the *single* named field.
     * For simple types this is a simple contains operation.
     *
     * For complex types this needs to validate for a put of a complex
     * type that *may* contain an indexed field.
     * Validation of such fields must be done later.
     *
     * In the case of a nested field name with dot-separated names,
     * this code simply checks that fieldName is one of the components of
     * the complex field (using String.contains()).
     */
    boolean isIndexPath(TablePath tablePath) {

        for (IndexField indexField : getIndexFields()) {
            if (indexField.equals(tablePath)) {
                    return true;
            }
        }
        return false;
    }

    /**
     * Creates a RecordDef for the flattened key fields. The rules for naming
     * the fields of the new RecordDef are:
     * 1. top-level atomic fields are left as-is (the field name, e.g. "a")
     * 2. paths into nested Records that do not involve arrays or maps are
     * left intact (e.g. "a.b.c")
     * 3. array elements turn into path-to-array[]
     * 4. map elements turn into path-to-map.values()
     * 5. map keys turn into path-to-map.keys()
     *
     * Index fields cannot contain more than one array or map which simplifies
     * the translations.
     */
    private RecordDefImpl createIndexKeyDef() {

        FieldMap fieldMap = createFieldMapForIndexKey();
        return new RecordDefImpl(fieldMap, null);
    }

    private RecordDefImpl createIndexEntryDef() {

        FieldMap fieldMap = createFieldMapForIndexKey();

        RecordDefImpl pkDef = table.getPrimKeyDef();
        int primKeySize = pkDef.getNumFields();

        for (int i = 0; i < primKeySize; ++i) {

            /*
             * Note: A '#' is placed in front of each prim key column name
             * because a prim key column may also be part of the index key,
             * so without the '#' we could end up with 2 record fields having
             * the same name. Other code that uses this indexEntryDef is
             * careful to access these fields positionally, rather than by name.
             */
            FieldDefImpl fdef = pkDef.getFieldDef(i);
            String fname = "#" + pkDef.getFieldName(i);
            final FieldMapEntry fme = new FieldMapEntry(fname, fdef);

            fieldMap.put(fme);
        }

        return new RecordDefImpl(fieldMap, null);
    }

    private FieldMap createFieldMapForIndexKey() {

        FieldMap fieldMap = new FieldMap();

        /*
         * Use the list of IndexField because it's already got complex
         * paths translated to a normalized form with use of [] in the
         * appropriate places.
         */
        for (int i = 0; i < numFields(); ++i) {

            IndexField indexField = getIndexPath(i);
            FieldDefImpl fdef = indexField.getType();
            String fname = newFields.get(i);
            final FieldMapEntry fme = new FieldMapEntry(fname, fdef);

            fieldMap.put(fme);
        }

        return fieldMap;
    }

    /**
     * For generic JSON indexes numerics, at least, will likely be all
     * converted to Number in indexes. This is TODO.
     */
    private static FieldValueImpl convertToJsonValue(FieldValueImpl val) {
        return val;
    }

    /**
     * Encapsulates a single field in an index, which may be simple or
     * complex.  Simple fields (e.g. "name") have a single component. Fields
     * that navigate into nested fields (e.g. "address.city") have multiple
     * components.  The state of whether a field is simple or complex is kept
     * by TablePath.
     *
     * IndexField adds this state:
     *   multiKeyField -- if this field results in a multi-key index this holds
     *     the portion of the field's path that leads to the FieldValue that
     *     makes it multi-key -- an array or map.  This is used as a cache to
     *     make navigation to that field easier.
     *   multiKeyType -- if multiKeyPath is set, this indicates if the field
     *     is a map key or map value field.
     * Arrays don't need additional state.
     *
     * Field names are case-insensitive, so strings are stored lower-case to
     * simplify case-insensitive comparisons.
     */
    public static class IndexField extends TablePath {

        /* the path to a multi-key field (map or array) */
        private IndexField multiKeyField;

        /*
         * The position of the multikey step (if any) within the multikey
         * index paths of this index.
         */
        private int multiKeyStepPos = -1;

        private MultiKeyType multiKeyType;

        /*
         * true if any part of the path is JSON. When this is true, if there
         * are remaining steps after the JSON field they are ignored because
         * they may or may not exist in any given JSON document
         */
        private boolean isJson;

        /*
         * true if the multi key field is the JSON field or sub field
         * of JSON field
         */
        private boolean isJsonMultiKey;

        private TablePath jsonFieldPath;

        /* the position in the key */
        private final int position;

        private FieldDefImpl declaredType;

        /*
         * The data type of this field. If the field is a json field with a
         * declared type, then this.type and this.declaredType are the same
         * type. Furthermore, if the user-declared type is GEOMETRY, both
         * this.type and this.declaredType are STRING, but this.isGeometry
         * is set to true.
         */
        private FieldDefImpl type;

        private boolean isPoint;

        private boolean isGeometry;

        /* the nullability of the field */
        private boolean nullable;

        /* ARRAY is not included because no callers need that information */
        private enum MultiKeyType { NONE, MAPKEY, MAPVALUE }

        /* public access for use by the query compiler */
        public IndexField(TableImpl table,
                          String field,
                          Type typecode,
                          int position) {

            super(table, field);

            multiKeyType = MultiKeyType.NONE;
            this.position = position;

            if (typecode == Type.GEOMETRY) {
                declaredType = FieldDefImpl.stringDef;
                isGeometry = true;
            } else if (typecode == Type.POINT) {
                declaredType = FieldDefImpl.stringDef;
                isPoint = true;
            } else {
                declaredType = (typecode == null ?
                                null :
                                getFieldDefForTypecode(typecode));
            }

            this.jsonFieldPath = null;
        }

        private IndexField(FieldMap fieldMap, String field, int position) {
            super(fieldMap, field);
            multiKeyType = MultiKeyType.NONE;
            this.position = position;
            this.declaredType = null;
            this.jsonFieldPath = null;
        }

        IndexField getMultiKeyField() {
            return multiKeyField;
        }

        public int getMultiKeyStepPos() {
            return multiKeyStepPos;
        }

        public boolean isMultiKey() {
            return multiKeyField != null;
        }

        private void setIsJson() {
            isJson = true;
        }

        public boolean isJson() {
            return isJson;
        }

        void setIsJsonMultiKey() {
            isJsonMultiKey = true;
        }

        boolean isJsonMultiKey() {
            return isJsonMultiKey;
        }

        boolean isJsonAsArray() {
            if (isJsonMultiKey()) {
                return !isMapKeys() && !isMapValues();
            }
            return false;
        }

        public boolean isPoint() {
            return isPoint;
        }

        public boolean isGeometry() {
            return isGeometry;
        }

        public int getPosition() {
            return position;
        }

        public FieldDefImpl getType() {
            return type;
        }

        public void setType(FieldDefImpl def) {
            type = def;
        }

        public FieldDefImpl getDeclaredType() {
            return declaredType;
        }

        boolean hasPreciseType() {
            return type.isPrecise();
        }

        boolean isNullable() {
            return nullable;
        }

        boolean mayHaveSpecialValue() {
            /*
             * Only top-level atomic columns may be non-nullable. Such columns
             * are non-EMPTY and non-json-null as well. So, this method is just
             * returns whether the index field is nullable.
             */
            return nullable;
        }

        private void setMultiKeyPath(int pos) {
            multiKeyField = new IndexField(getFieldMap(), null, position);
            for (int i = 0; i < pos; ++i) {
                StepInfo si = getStepInfo(i);
                multiKeyField.addStepInfo(new StepInfo(si));
            }
            multiKeyStepPos = pos;
        }

        public boolean isMapKeys() {
            return multiKeyType == MultiKeyType.MAPKEY;
        }

        private void setIsMapKeys() {
            multiKeyType = MultiKeyType.MAPKEY;
        }

        public boolean isMapValues() {
            return multiKeyType == MultiKeyType.MAPVALUE;
        }

        private void setIsMapValues() {
            multiKeyType = MultiKeyType.MAPVALUE;
        }

        public void setNullable(boolean nullable) {
            this.nullable = nullable;
        }

        private void setJsonFieldPath(int pos) {
            jsonFieldPath = new TablePath(getFieldMap(), (String)null);
            for (int i = 0; i < pos; ++i) {
                StepInfo si = getStepInfo(i);
                jsonFieldPath.addStepInfo(new StepInfo(si));
            }
        }

        TablePath getJsonFieldPath() {
            return jsonFieldPath;
        }

        private static FieldDefImpl getFieldDefForTypecode(FieldDef.Type code) {
            switch (code) {
            case INTEGER:
                return FieldDefImpl.integerDef;
            case LONG:
                return FieldDefImpl.longDef;
            case DOUBLE:
                return FieldDefImpl.doubleDef;
            case BOOLEAN:
                return FieldDefImpl.booleanDef;
            case STRING:
                return FieldDefImpl.stringDef;
            case NUMBER:
                return FieldDefImpl.numberDef;
            default:
                throw new IllegalArgumentException(
                    "Invalid type for JSON index field: " + code);
            }
        }
    }

    @Override
    public Index.IndexType getType() {
        if (getAnnotationsInternal() == null) {
            return Index.IndexType.SECONDARY;
        }
        return Index.IndexType.TEXT;
    }

    private boolean isTextIndex() {
        return getType() == Index.IndexType.TEXT;
    }

    public static void populateMapFromAnnotatedFields(
         List fields,
         List fieldNames,
         Map annotations) {

    	for (AnnotatedField f : fields) {
            String fieldName = f.getFieldName();
            fieldNames.add(fieldName);
            annotations.put(fieldName, f.getAnnotation());
    	}
    }

    /**
     * This lightweight class stores an index field, along with
     * an annotation.  Not all index types require annotations;
     * It is used for the mapping specifier in full-text indexes.
     */
    public static class AnnotatedField implements Serializable {

        private static final long serialVersionUID = 1L;

        private final String fieldName;

        private final String annotation;

        public AnnotatedField(String fieldName, String annotation) {
            assert(fieldName != null);
            this.fieldName = fieldName;
            this.annotation = annotation;
        }

        /**
         * The name of the indexed field.
         */
        public String getFieldName() {
            return fieldName;
        }

        /**
         *  The field's annotation.  In Text indexes, this is the ES mapping
         *  specification, which is a JSON string and may be null.
         */
        public String getAnnotation() {
            return annotation;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }

            AnnotatedField other = (AnnotatedField) obj;

            if (! fieldName.equals(other.fieldName)) {
                return false;
            }

            return (annotation == null ?
                    other.annotation == null :
                    JsonUtils.jsonStringsEqual(annotation, other.annotation));
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + fieldName.hashCode();
            if (annotation != null) {
                result = prime * result + annotation.hashCode();
            }
            return result;
        }
    }

    @Override
    public String getAnnotationForField(String fieldName) {
        if (isTextIndex() == false) {
            return null;
        }
        return getAnnotationsInternal().get(fieldName);
    }

    public RowImpl deserializeRow(byte[] keyBytes, byte[] valueBytes) {
        return table.createRowFromBytes(keyBytes, valueBytes, false);
    }

    /**
     * Formats the index.
     * @param asJson true if output should be JSON, otherwise tabular.
     */
    public String formatIndex(boolean asJson) {
        if (asJson) {
            ObjectWriter writer = JsonUtils.createWriter(true);
            ObjectNode o = JsonUtils.createObjectNode();
            toJsonNode(o);
            /*
             * Format the JSON into a string
             */
            try {
                return writer.writeValueAsString(o);
            } catch (IOException ioe) {
                throw new IllegalArgumentException
                    ("Failed to serialize index description: " +
                     ioe.getMessage());
            }
        }
        return TabularFormatter.formatIndex(this);
    }

    private boolean areIndicatorBytesEnabled() {
        if (Boolean.getBoolean(INDEX_NULL_DISABLE)) {
            return false;
        }
        return true;
    }

    public int getIndexVersion() {
        return Integer.getInteger(INDEX_SERIAL_VERSION, INDEX_VERSION_CURRENT);
    }

    /**
     * This is directly from JE's com.sleepycat.je.tree.Key class and is the
     * default byte comparator for JE's btree.
     *
     * Compare using a default unsigned byte comparison.
     */
    static int compareUnsignedBytes(byte[] key1,
                                    int off1,
                                    int len1,
                                    byte[] key2,
                                    int off2,
                                    int len2) {
        int limit = Math.min(len1, len2);

        for (int i = 0; i < limit; i++) {
            byte b1 = key1[i + off1];
            byte b2 = key2[i + off2];
            if (b1 == b2) {
                continue;
            }
            /*
             * Remember, bytes are signed, so convert to shorts so that we
             * effectively do an unsigned byte comparison.
             */
            return (b1 & 0xff) - (b2 & 0xff);
        }

        return (len1 - len2);
    }

    static int compareUnsignedBytes(byte[] key1, byte[] key2) {
        return compareUnsignedBytes(key1, 0, key1.length, key2, 0, key2.length);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy