oracle.kv.impl.api.table.IndexKeyImpl Maven / Gradle / Ivy
/*-
* 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 java.math.BigDecimal;
import java.sql.Timestamp;
import oracle.kv.table.FieldDef;
import oracle.kv.table.FieldDef.Type;
import oracle.kv.table.FieldValue;
import oracle.kv.table.Index;
import oracle.kv.table.IndexKey;
import org.codehaus.jackson.util.CharTypes;
/**
* IndexKeyImpl is a flattened instance of RecordValueImpl where nested
* fields declared in an index are translated into simple field names. This
* simplifies manipulation of complex index keys by turning them into single-
* level records. The type definitions of the fields in this record are
* atomic, indexable types such as Integer, String, and Boolean.
*
* Instances of IndexKeyImpl are generated by calls to Index.createIndexKey()
* which calls (static) IndexKeyImpl createIndexKey(IndexImpl index).
*
* Complex index paths are represented as these strings in IndexKeyImpl:
* 1. array element index → path-to-array[], e.g. this_is_an_array[]
* 2. map element index → path-to-map[], e.g. this_is_a_map[]
* 3. map key index → "keys(path-to-member), e.g. keys(this_is_a_map)
* 4. record member index → path-to-member, e.g. address.city
*
* There is still the special case of an index on a specific map key, which
* is not a multi-key index. These are represented as
* path-to-map.indexedKeyString, e.g. this_is_a_map.indexedKeyString
* NOTE: should this be path-to-map[indexedKeyString] instead?
*/
public class IndexKeyImpl extends RecordValueImpl implements IndexKey {
private static final long serialVersionUID = 1L;
final IndexImpl index;
/**
* The RecordDef associated with an IndexKeyImpl is that of its table.
*/
IndexKeyImpl(final IndexImpl index,
final RecordDefImpl indexKeyDef) {
super(indexKeyDef);
this.index = index;
}
private IndexKeyImpl(IndexKeyImpl other) {
super(other);
this.index = other.index;
}
/**
* Return the Index associated with this key
*/
@Override
public Index getIndex() {
return index;
}
@Override
public IndexKeyImpl clone() {
return new IndexKeyImpl(this);
}
@Override
public IndexKey asIndexKey() {
/**
* Override putField to add validation
*/
return this;
}
@Override
public boolean isIndexKey() {
return true;
}
@Override
public boolean equals(Object other) {
if (other instanceof IndexKeyImpl) {
return super.equals(other);
}
return false;
}
@Override
public int hashCode() {
return super.hashCode();
}
/**
* Validate the index key. Rules:
* 1. Fields must be in the index. This is guaranteed by the schema
* associated with this object.
* 2. Fields must be specified in order. If a field "to the right"
* in the index definition is set, all fields to its "left" must also
* be present.
*/
@Override
public void validate() {
validateIndexFields();
}
public int getKeySize() {
return index.serializeIndexKey(this).length;
}
TableImpl getTable() {
return index.getTable();
}
public IndexImpl getIndexImpl() {
return index;
}
/**
* Return true if all fields in the index are specified. This method
* should be called only after the key has been validated.
*/
public boolean isComplete() {
return (size() == getNumFields());
}
/**
* This function behaves like adding "one" to the entire index key. That
* is, it increments the least significant field but if that field "wraps"
* in that it's already at its maximum in terms of data type, such as
* Integer.MAX_VALUE then increment the next more significant field and
* set that field to its minimum value.
*
* If the value would wrap and there are no more significant fields then
* return false, indicating that the "next" value is actually the end
* of the index, period.
*
* This code is used to implement inclusive/exclusive semantics.
*
* Indexes that include a map key as a field are slightly more complicated.
* In this case the key needs to be incremented and put back into the map.
* In order to avoid multiple keys in the map the original key must be
* removed from the map.
*/
public boolean incrementIndexKey() {
RecordDefImpl def = getDefinition();
FieldValue[] values = new FieldValue[def.getNumFields()];
int fieldIndex;
for (fieldIndex = 0; fieldIndex < def.getNumFields(); ++fieldIndex) {
values[fieldIndex] = get(fieldIndex);
if (values[fieldIndex] == null) {
break;
}
}
/*
* At least one field must exist. Assert that and move back to the
* target field.
*/
assert fieldIndex > 0;
--fieldIndex;
/*
* Increment and reset. If the current field returns null, indicating
* that it will wrap its value, set it to its minimum value and move to
* the next more significant field. If there are none, return false
* indicating that there are no larger keys in the index that match the
* key.
*/
boolean isJsonField = index.getIndexFields().get(fieldIndex).isJson();
FieldValueImpl fvi =
getNextValue((FieldValueImpl)values[fieldIndex],
index.fieldMayHaveSpecialValue(fieldIndex),
isJsonField);
while (fvi == null) {
fvi = getMinimumValue((FieldValueImpl)values[fieldIndex],
def.getField(fieldIndex));
put(def.getFieldName(fieldIndex), fvi);
/*
* Move to next more significant field if it exists
*/
--fieldIndex;
if (fieldIndex >= 0) {
fvi = getNextValue((FieldValueImpl)values[fieldIndex],
index.fieldMayHaveSpecialValue(fieldIndex),
isJsonField);
} else {
/*
* Failed to increment
*/
return false;
}
}
assert fvi != null && fieldIndex >= 0;
put(fieldIndex, fvi);
return true;
}
@Override
protected String getClassNameForError() {
return "IndexKey";
}
/**
* Inserts the field at the given position, or updates its value if the
* field exists already. The field must be of type JSON.
*/
@Override
public IndexKeyImpl putJsonNull(int pos) {
if (!index.getIndexFields().get(pos).isJson()) {
String fname = getFieldName(pos);
throw new IllegalArgumentException(
"Field \"" + fname + "\" is not JSON");
}
putInternal(pos, NullJsonValueImpl.getInstance());
return this;
}
/**
* Inserts the field at the given position, or updates its value if the
* field exists already.
*/
@Override
public IndexKeyImpl putEMPTY(int pos) {
putInternal(pos, EmptyValueImpl.getInstance());
return this;
}
/**
* Puts an EMPTY value in the named field, silently overwriting existing
* value.
*/
@Override
public IndexKeyImpl putEMPTY(String name) {
int pos = getFieldPos(name);
return putEMPTY(pos);
}
/**
* Returns the next value of the given value:
* If NULLs are allowed in index key, follow the rule of NULL last, the
* next value of MAX VALUE for each type is NULL.
* Null values (SQL or JSON null) cannot be incremented.
*/
private FieldValueImpl getNextValue(FieldValueImpl value,
boolean allowNull,
boolean isJsonField) {
if (value.isNull()) {
return null;
}
FieldValueImpl next;
if (value.isEMPTY()) {
next = isJsonField ? NullJsonValueImpl.getInstance() :
NullValueImpl.getInstance();
} else if (value.isJsonNull()) {
next = NullValueImpl.getInstance();
} else {
next = value.getNextValue();
if (next == null && allowNull) {
next = EmptyValueImpl.getInstance();
}
}
return next;
}
/**
* Returns the minimal field value..
*/
private FieldValueImpl getMinimumValue(FieldValueImpl value, FieldDef def) {
/*
* The minimum value for a JSON type that is null is itself.
*/
if ((value.isNull() || value.isJsonNull()) && def.isJson()) {
return value;
}
if (value.isNull()) {
return getMinValueOfType(def);
}
return value.getMinimumValue();
}
/**
* Returns the minimal field value of the specified type.
*/
private FieldValueImpl getMinValueOfType(FieldDef def) {
Type type = def.getType();
FieldValue fv = null;
switch(type) {
case INTEGER:
fv = def.createInteger(0);
break;
case LONG:
fv = def.createLong(0L);
break;
case FLOAT:
fv = def.createFloat(0.0f);
break;
case DOUBLE:
fv = def.createDouble(0.0d);
break;
case NUMBER:
fv = def.createNumber(BigDecimal.ZERO);
break;
case STRING:
fv = def.createString("");
break;
case ENUM:
return ((EnumDefImpl)def).createEnum(0);
case TIMESTAMP:
fv = ((TimestampDefImpl)def).createTimestamp(new Timestamp(0));
break;
case BOOLEAN:
fv = def.createBoolean(false);
break;
default:
throw new IllegalArgumentException(
"Unexpected type for index key: " + type);
}
return ((FieldValueImpl)fv).getMinimumValue();
}
/*
* Must overwrite the RecordValueImpl implementation of this method,
* because the field names of index keys are not just simple identifiers,
* and as a result, CharTypes.appendQuoted() must be used to print them.
*/
@Override
public void toStringBuilder(StringBuilder sb) {
boolean wroteFirstField = false;
sb.append('{');
for (int i = 0; i < getNumFields(); ++i) {
String fieldName = getFieldName(i);
FieldValueImpl val = get(i);
if (val != null) {
if (wroteFirstField) {
sb.append(',');
}
sb.append('\"');
CharTypes.appendQuoted(sb, fieldName);
sb.append('\"');
sb.append(':');
val.toStringBuilder(sb);
wroteFirstField = true;
}
}
sb.append('}');
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy