blob: 28e6d57551d338ca335639de3d393e846ae68e14 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ignite.internal.processors.cache.index;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.apache.ignite.cache.query.SqlFieldsQuery;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.testframework.GridTestUtils;
import org.h2.util.DateTimeUtils;
import org.jetbrains.annotations.Nullable;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import static org.apache.ignite.internal.processors.cache.index.BasicSqlTypesIndexTest.IndexType.PK;
import static org.apache.ignite.internal.processors.cache.index.BasicSqlTypesIndexTest.IndexType.SECONDARY_ASC;
import static org.apache.ignite.internal.processors.cache.index.BasicSqlTypesIndexTest.IndexType.SECONDARY_DESC;
import static org.apache.ignite.internal.processors.query.QueryUtils.KEY_FIELD_NAME;
import static org.apache.ignite.internal.processors.query.h2.H2TableDescriptor.PK_IDX_NAME;
/**
* Basic tests for different types of indexed data
* for tables created through SQL API.
*/
public class BasicSqlTypesIndexTest extends AbstractIndexingCommonTest {
/** Count of entries that should be preloaded to the table. */
private static final int DATSET_SIZE = 1_000;
/** Table ID counter. */
private static final AtomicInteger TBL_ID = new AtomicInteger();
/** Template of CREATE TABLE query with pk and value columns. */
private static final String CREATE_TBL_PK_ONLY_TEMPLATE =
"CREATE TABLE \"%s\" (idxVal %s PRIMARY KEY, val INT) WITH \"%s\"";
/** Template of CREATE TABLE query with pk, value and additional column for indexing. */
private static final String CREATE_TBL_SECONDARY_INDEX_TEMPLATE =
"CREATE TABLE \"%s\" (id INT PRIMARY KEY, idxVal %s, val INT) WITH \"%s\"";
/** Template of CREATE INDEX query for additional column. */
private static final String CREATE_SECONDARY_INDEX_TEMPLATE = "CREATE INDEX \"%s\" ON \"%s\"(idxVal %s)";
/** Template of SELECT query with filtering by range and ordering by indexed column. */
private static final String SELECT_ORDERED_RANGE_TEMPLATE =
"SELECT val FROM \"%s\" USE INDEX(\"%s\") WHERE %s <= ? ORDER BY %s ASC";
/** Template of SELECT query with filtering by indexed column. */
private static final String SELECT_VALUE_TEMPLATE =
"SELECT val, idxVal FROM \"%s\" USE INDEX(\"%s\") WHERE %s = ?";
/** Template of INSERT query for two-column table. See {@link #CREATE_TBL_PK_ONLY_TEMPLATE}. */
private static final String INSERT_PK_ONLY_TEMPLATE = "INSERT INTO \"%s\" (idxVal, val) VALUES (?, ?)";
/** Template of INSERT query for three-column table. See {@link #CREATE_TBL_SECONDARY_INDEX_TEMPLATE}. */
private static final String INSERT_SECONDARY_TEMPLATE = "INSERT INTO \"%s\" (id, idxVal, val) VALUES (?, ?, ?)";
/** Template of UPDATE query for updating value by indexed column. */
private static final String UPDATE_VAL_TEMPLATE = "UPDATE \"%s\" SET val=? WHERE idxVal=?";
/** Precision for generator of decimal values. */
private int decimalPrecision;
/** Max length for generator of string values. */
private int maxStrLen;
/** {@inheritDoc} */
@Override protected void beforeTestsStarted() throws Exception {
super.beforeTestsStarted();
startGridsMultiThreaded(2);
}
/** */
@Before
public void clearState() {
decimalPrecision = -1;
maxStrLen = 40;
}
/** */
@Test
public void testSqlBooleanTypeIndex() {
String idxTypeStr = "BOOLEAN";
Class<Boolean> idxCls = Boolean.class;
createPopulateAndVerify(idxTypeStr, idxCls, Boolean::compareTo, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Boolean::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, Boolean::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Boolean::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlBigintTypeIndex() {
String idxTypeStr = "BIGINT";
Class<Long> idxCls = Long.class;
createPopulateAndVerify(idxTypeStr, idxCls, Long::compareTo, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Long::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, Long::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Long::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlDecimalTypeIndex() {
String idxTypeStr = "DECIMAL";
Class<BigDecimal> idxCls = BigDecimal.class;
createPopulateAndVerify(idxTypeStr, idxCls, BigDecimal::compareTo, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, BigDecimal::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, BigDecimal::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, BigDecimal::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlDecimalFixedPrecisionTypeIndex() {
decimalPrecision = 3;
String idxTypeStr = "DECIMAL(20,3)";
Class<BigDecimal> idxCls = BigDecimal.class;
createPopulateAndVerify(idxTypeStr, idxCls, BigDecimal::compareTo, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, BigDecimal::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, BigDecimal::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, BigDecimal::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlNumericTypeIndex() {
decimalPrecision = 0;
String idxTypeStr = "NUMERIC(10)";
Class<BigDecimal> idxCls = BigDecimal.class;
createPopulateAndVerify(idxTypeStr, idxCls, BigDecimal::compareTo, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, BigDecimal::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, BigDecimal::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, BigDecimal::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlDoubleTypeIndex() {
String idxTypeStr = "DOUBLE";
Class<Double> idxCls = Double.class;
createPopulateAndVerify(idxTypeStr, idxCls, Double::compareTo, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Double::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, Double::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Double::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlIntTypeIndex() {
String idxTypeStr = "INT";
Class<Integer> idxCls = Integer.class;
createPopulateAndVerify(idxTypeStr, idxCls, Integer::compareTo, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Integer::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, Integer::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Integer::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlRealTypeIndex() {
String idxTypeStr = "REAL";
Class<Float> idxCls = Float.class;
createPopulateAndVerify(idxTypeStr, idxCls, Float::compareTo, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Float::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, Float::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Float::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlTinyintTypeIndex() {
String idxTypeStr = "TINYINT";
Class<Byte> idxCls = Byte.class;
createPopulateAndVerify(idxTypeStr, idxCls, Byte::compareTo, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Byte::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, Byte::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Byte::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlSmallintTypeIndex() {
String idxTypeStr = "SMALLINT";
Class<Short> idxCls = Short.class;
createPopulateAndVerify(idxTypeStr, idxCls, Short::compareTo, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Short::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, Short::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Short::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlVarcharTypeIndex() {
String idxTypeStr = "VARCHAR";
Class<String> idxCls = String.class;
createPopulateAndVerify(idxTypeStr, idxCls, String::compareTo, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, String::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, String::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, String::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlCharTypeIndex() {
maxStrLen = 10;
String idxTypeStr = "CHAR(10)";
Class<String> idxCls = String.class;
createPopulateAndVerify(idxTypeStr, idxCls, String::compareTo, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, String::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, String::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, String::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlDateTypeIndex() {
String idxTypeStr = "DATE";
Class<Date> idxCls = Date.class;
// TODO: https://issues.apache.org/jira/browse/IGNITE-8552
// createPopulateAndVerify(idxTypeStr, idxCls, Date::compareTo, PK, "BACKUPS=1");
// createPopulateAndVerify(idxTypeStr, idxCls, Date::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, Date::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Date::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlTimeTypeIndex() {
String idxTypeStr = "TIME";
Class<Time> idxCls = Time.class;
Comparator<Time> comp = new Comparator<Time>() {
@Override public int compare(Time o1, Time o2) {
GregorianCalendar cal = new GregorianCalendar();
long l1 = DateTimeUtils.convertTime(o1, cal).getNanos();
long l2 = DateTimeUtils.convertTime(o2, cal).getNanos();
return Long.compare(l1, l2);
}
};
createPopulateAndVerify(idxTypeStr, idxCls, comp, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, comp, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, comp, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, comp, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlTimestampTypeIndex() {
String idxTypeStr = "TIMESTAMP";
Class<Timestamp> idxCls = Timestamp.class;
createPopulateAndVerify(idxTypeStr, idxCls, Timestamp::compareTo, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Timestamp::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, Timestamp::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, Timestamp::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlBynaryTypeIndex() {
String idxTypeStr = "BINARY";
Class<byte[]> idxCls = byte[].class;
createPopulateAndVerify(idxTypeStr, idxCls, F::compareArrays, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, F::compareArrays, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, F::compareArrays, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, F::compareArrays, SECONDARY_ASC, "BACKUPS=1");
}
/** */
@Test
public void testSqlUuidTypeIndex() {
String idxTypeStr = "UUID";
Class<UUID> idxCls = UUID.class;
createPopulateAndVerify(idxTypeStr, idxCls, UUID::compareTo, PK, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, UUID::compareTo, PK, "BACKUPS=1,AFFINITY_KEY=idxVal");
createPopulateAndVerify(idxTypeStr, idxCls, UUID::compareTo, SECONDARY_DESC, "BACKUPS=1");
createPopulateAndVerify(idxTypeStr, idxCls, UUID::compareTo, SECONDARY_ASC, "BACKUPS=1");
}
/**
* Executes test scenario: <ul>
* <li>Create table</li>
* <li>Create necessary indexes</li>
* <li>Populate table with random data</li>
* <li>Verify range query on created table</li>
* <li>Verify that table stores the same data as the generated dataset</li>
* <li>Drop table</li>
* </ul>
*
*
* @param idxTypeStr Index type in string (e.g. 'INT').
* @param idxTypeCls Index type class.
* @param idxType Whether index should be primary key or not.
* @param benefits Benefits will be used for table creation
* (i.e. {@code CREATE TABLE friends() WITH "<benefits>"}).
* @param <T> Java type mapping of the indexed column.
*/
private <T> void createPopulateAndVerify(String idxTypeStr, Class<T> idxTypeCls, Comparator<T> comp,
IndexType idxType, @Nullable String benefits) {
String tblName = idxTypeStr + "_TBL" + TBL_ID.incrementAndGet();
try {
String createTblSql = String.format(
idxType == PK ? CREATE_TBL_PK_ONLY_TEMPLATE : CREATE_TBL_SECONDARY_INDEX_TEMPLATE,
tblName, idxTypeStr, benefits != null ? benefits : ""
);
execSql(createTblSql);
String idxName;
String idxFieldName;
if (idxType != PK) {
idxName = "idxVal_idx";
idxFieldName = "idxVal";
execSql(String.format(CREATE_SECONDARY_INDEX_TEMPLATE, idxName, tblName,
idxType == SECONDARY_ASC ? "ASC" : "DESC"));
}
else {
idxName = PK_IDX_NAME;
idxFieldName = KEY_FIELD_NAME;
}
Map<T, Integer> data = new TreeMap<>(comp);
populateTable(data, tblName, idxTypeCls, idxType == PK);
verifyRange(data, tblName, idxFieldName, idxName, comp);
verifyEach(data, tblName, idxFieldName, idxName);
}
finally {
execSql("DROP TABLE IF EXISTS \"" + tblName + "\"");
}
}
/**
* Populate table with random data.
*
* @param data Map which will be used to store all generated data.
* @param tblName Table name that should be populated.
* @param idxValCls Class of indexed value. Used for generating random value
* of the required type.
* @param pk Whether indexed value is primary key or not.
* @param <T> Java type mapping of the indexed column.
*/
private <T> void populateTable(Map<T, Integer> data, String tblName, Class<T> idxValCls, boolean pk) {
for (int i = 0; i < DATSET_SIZE; i++) {
int id = nextVal(Integer.class);
T idxVal = nextVal(idxValCls);
int val = nextVal(Integer.class);
if (data.put(idxVal, val) == null) {
if (pk)
execSql(String.format(INSERT_PK_ONLY_TEMPLATE, tblName), idxVal, val);
else
execSql(String.format(INSERT_SECONDARY_TEMPLATE, tblName), id, idxVal, val);
}
else
execSql(String.format(UPDATE_VAL_TEMPLATE, tblName), val, idxVal);
}
}
/**
* Generates random value for the given class.
*
* @param cls Class of the required value.
* @param <T> Java type mapping of the indexed column.
*
* @return Generated value.
*/
private <T> T nextVal(Class<T> cls) {
if (cls.isAssignableFrom(Boolean.class))
return cls.cast(ThreadLocalRandom.current().nextBoolean());
if (cls.isAssignableFrom(Byte.class))
return cls.cast((byte)ThreadLocalRandom.current().nextInt());
if (cls.isAssignableFrom(Short.class))
return cls.cast((short)ThreadLocalRandom.current().nextInt());
if (cls.isAssignableFrom(Integer.class))
return cls.cast(ThreadLocalRandom.current().nextInt());
if (cls.isAssignableFrom(Long.class))
return cls.cast(ThreadLocalRandom.current().nextLong());
if (cls.isAssignableFrom(Float.class))
return cls.cast(ThreadLocalRandom.current().nextFloat());
if (cls.isAssignableFrom(Double.class))
return cls.cast(ThreadLocalRandom.current().nextDouble());
if (cls.isAssignableFrom(BigDecimal.class)) {
BigDecimal bd = new BigDecimal(ThreadLocalRandom.current().nextDouble());
if (decimalPrecision >= 0)
bd = bd.setScale(decimalPrecision, RoundingMode.HALF_UP);
return cls.cast(bd);
}
if (cls.isAssignableFrom(String.class))
return cls.cast(GridTestUtils.randomString(ThreadLocalRandom.current(), 1, maxStrLen));
if (cls.isAssignableFrom(Date.class))
return cls.cast(new Date(ThreadLocalRandom.current().nextLong()));
if (cls.isAssignableFrom(Time.class))
return cls.cast(new Time(ThreadLocalRandom.current().nextLong()));
if (cls.isAssignableFrom(Timestamp.class))
return cls.cast(new Timestamp(ThreadLocalRandom.current().nextLong()));
if (cls.isAssignableFrom(UUID.class))
return cls.cast(new UUID(
ThreadLocalRandom.current().nextLong(),
ThreadLocalRandom.current().nextLong()
));
if (cls.isAssignableFrom(byte[].class))
return cls.cast(GridTestUtils.randomString(ThreadLocalRandom.current(), 1, maxStrLen).getBytes());
throw new IllegalStateException("There is no generator for class=" + cls.getSimpleName());
}
/**
* Verifies range querying.
*
* @param data Testing dataset.
* @param tblName Name of the table from which values should be queried.
* @param idxFieldName Name of the indexed field.
* @param idxName Name of the index.
* @param comp Comparator.
* @param <T> Java type mapping of the indexed column.
*/
private <T> void verifyRange(Map<T, Integer> data, String tblName,
String idxFieldName, String idxName, Comparator<T> comp) {
T val = getRandom(data.keySet());
List<List<?>> res = execSql(String.format(SELECT_ORDERED_RANGE_TEMPLATE, tblName, idxName, idxFieldName, idxFieldName), val);
List<Integer> exp = data.entrySet().stream()
.filter(e -> comp.compare(e.getKey(), val) <= 0)
.sorted((e1, e2) -> comp.compare(e1.getKey(), e2.getKey()))
.map(Map.Entry::getValue)
.collect(Collectors.toList());
List<Integer> act = res.stream()
.flatMap(List::stream)
.map(e -> (Integer)e)
.collect(Collectors.toList());
Assert.assertEquals(exp, act);
}
/**
* Verifies that table content equals to generated dataset.
*
* @param data Testing dataset.
* @param tblName Name of the table from which values should be queried.
* @param idxFieldName Name of the indexed field.
* @param idxName Name of the index.
* @param <T> Java type mapping of the indexed column.
*/
private <T> void verifyEach(Map<T, Integer> data, String tblName, String idxFieldName, String idxName) {
for (Map.Entry<T, Integer> entry : data.entrySet()) {
List<List<?>> res = execSql(
String.format(SELECT_VALUE_TEMPLATE, tblName, idxName, idxFieldName), entry.getKey()
);
Assert.assertFalse("Result should not be empty", res.isEmpty());
Assert.assertFalse("Result should contain at least one column", res.get(0).isEmpty());
Assert.assertEquals(entry.getValue(), res.get(0).get(0));
}
}
/**
* Returns random element from collection.
*
* @param col Collection from which random element should be returned.
* @param <T> Java type mapping of the indexed column.
*
* @return Random element from given collection.
*/
private <T> T getRandom(Collection<T> col) {
int rndIdx = ThreadLocalRandom.current().nextInt(col.size());
int i = 0;
for (T el : col) {
if (i++ == rndIdx)
return el;
}
return null;
}
/**
* @param qry Query.
* @param args Args.
*/
private List<List<?>> execSql(String qry, Object... args) {
return grid(0).context().query().querySqlFields(new SqlFieldsQuery(qry).setArgs(args), false).getAll();
}
/** Type of the index to create. */
enum IndexType {
/** Primary key. */
PK,
/** Secondary index with ascending sorting. */
SECONDARY_ASC,
/** Secondary index with descending sorting. */
SECONDARY_DESC
}
}