diff --git a/client/src/com/aerospike/client/AerospikeClient.java b/client/src/com/aerospike/client/AerospikeClient.java index 06f2e0cf6..255d3b1d2 100644 --- a/client/src/com/aerospike/client/AerospikeClient.java +++ b/client/src/com/aerospike/client/AerospikeClient.java @@ -5452,6 +5452,9 @@ private String buildCreateIndexInfoCommand( Version currentServerVersion = node.getServerVersion(); String createIndexCommand = currentServerVersion.isGreaterOrEqual(Version.SERVER_VERSION_8_1) ? "sindex-create:namespace=": "sindex-create:ns="; + // Server versions 8.1.3+ use the "integer" index type instead of "numeric". + indexType = resolveIndexType(indexType, currentServerVersion); + sb.append(createIndexCommand); sb.append(namespace); @@ -5511,6 +5514,23 @@ private String buildCreateIndexInfoCommand( return sb.toString(); } + /** + * Map the requested index type to the type the target server understands. + * Server versions 8.1.3+ use "integer" instead of "numeric", so a NUMERIC + * request is upgraded to INTEGER on those servers and an INTEGER request is + * downgraded to NUMERIC on older servers. All other index types are returned + * unchanged. Package-private for unit testing. + */ + static IndexType resolveIndexType(IndexType indexType, Version serverVersion) { + if (indexType == IndexType.NUMERIC && serverVersion.isGreaterOrEqual(Version.SERVER_VERSION_8_1_3)) { + return IndexType.INTEGER; + } + if (indexType == IndexType.INTEGER && serverVersion.isLessThan(Version.SERVER_VERSION_8_1_3)) { + return IndexType.NUMERIC; + } + return indexType; + } + private String buildDropIndexInfoCommand(Node node, String namespace, String setName, String indexName) { StringBuilder sb = new StringBuilder(500); Version currentServerVersion = node.getServerVersion(); diff --git a/client/src/com/aerospike/client/query/IndexType.java b/client/src/com/aerospike/client/query/IndexType.java index 5cfb445c4..b3d7f27d4 100644 --- a/client/src/com/aerospike/client/query/IndexType.java +++ b/client/src/com/aerospike/client/query/IndexType.java @@ -21,7 +21,7 @@ */ public enum IndexType { /** - * Number index. + * Number index. Use {@link #INTEGER} for server versions 8.1.3+. */ NUMERIC, @@ -38,5 +38,11 @@ public enum IndexType { /** * 2-dimensional spherical geospatial index. */ - GEO2DSPHERE; + GEO2DSPHERE, + + /** + * Integer index. Requires server version 8.1.3+. Use {@link #NUMERIC} for + * server versions prior to 8.1.3. + */ + INTEGER; } diff --git a/client/src/com/aerospike/client/util/Version.java b/client/src/com/aerospike/client/util/Version.java index 4df0cdbbc..25fe9f763 100644 --- a/client/src/com/aerospike/client/util/Version.java +++ b/client/src/com/aerospike/client/util/Version.java @@ -27,6 +27,7 @@ public final class Version implements Comparable { public static final Version SERVER_VERSION_8_1 = new Version(8, 1, 0, 0); public static final Version SERVER_VERSION_8_1_2 = new Version(8, 1, 2, 0); + public static final Version SERVER_VERSION_8_1_3 = new Version(8, 1, 3, 0); public static final Version SERVER_VERSION_PSCAN = new Version(4, 9, 0, 3); public static final Version SERVER_VERSION_QUERY_SHOW = new Version(5, 7, 0, 0); public static final Version SERVER_VERSION_PQUERY_BATCH_ANY = new Version(6, 0, 0, 0); diff --git a/test/src/com/aerospike/client/AerospikeClientIndexTypeTest.java b/test/src/com/aerospike/client/AerospikeClientIndexTypeTest.java new file mode 100644 index 000000000..60b7932c5 --- /dev/null +++ b/test/src/com/aerospike/client/AerospikeClientIndexTypeTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2026 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. + * + * Licensed 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 com.aerospike.client; + +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +import com.aerospike.client.query.IndexType; +import com.aerospike.client.util.Version; + +/** + * Server-independent unit tests for {@link AerospikeClient#resolveIndexType}. + * + * Server versions 8.1.3+ use the "integer" index type instead of "numeric". + * The client transparently maps between the two based on the target server + * version. This is the only place the mapping is observable: on the server, + * "numeric" and "integer" collapse to the same internal type, so the created + * index carries no record of which spelling was used. + */ +public class AerospikeClientIndexTypeTest { + private static Version version(int major, int minor, int patch, int build) { + return new Version(major, minor, patch, build); + } + + @Test + public void numericUpgradesToIntegerOn813() { + // Exact boundary. + assertSame(IndexType.INTEGER, AerospikeClient.resolveIndexType(IndexType.NUMERIC, version(8, 1, 3, 0))); + } + + @Test + public void numericUpgradesToIntegerAbove813() { + assertSame(IndexType.INTEGER, AerospikeClient.resolveIndexType(IndexType.NUMERIC, version(8, 1, 4, 0))); + assertSame(IndexType.INTEGER, AerospikeClient.resolveIndexType(IndexType.NUMERIC, version(9, 0, 0, 0))); + // Build component past the boundary still counts as >= 8.1.3.0. + assertSame(IndexType.INTEGER, AerospikeClient.resolveIndexType(IndexType.NUMERIC, version(8, 1, 3, 5))); + } + + @Test + public void numericUnchangedBelow813() { + assertSame(IndexType.NUMERIC, AerospikeClient.resolveIndexType(IndexType.NUMERIC, version(8, 1, 2, 0))); + assertSame(IndexType.NUMERIC, AerospikeClient.resolveIndexType(IndexType.NUMERIC, version(8, 1, 2, 99))); + assertSame(IndexType.NUMERIC, AerospikeClient.resolveIndexType(IndexType.NUMERIC, version(8, 0, 0, 0))); + assertSame(IndexType.NUMERIC, AerospikeClient.resolveIndexType(IndexType.NUMERIC, version(4, 9, 0, 3))); + } + + @Test + public void integerUnchangedOnOrAbove813() { + assertSame(IndexType.INTEGER, AerospikeClient.resolveIndexType(IndexType.INTEGER, version(8, 1, 3, 0))); + assertSame(IndexType.INTEGER, AerospikeClient.resolveIndexType(IndexType.INTEGER, version(9, 0, 0, 0))); + } + + @Test + public void integerDowngradesToNumericBelow813() { + assertSame(IndexType.NUMERIC, AerospikeClient.resolveIndexType(IndexType.INTEGER, version(8, 1, 2, 0))); + assertSame(IndexType.NUMERIC, AerospikeClient.resolveIndexType(IndexType.INTEGER, version(8, 0, 0, 0))); + assertSame(IndexType.NUMERIC, AerospikeClient.resolveIndexType(IndexType.INTEGER, version(4, 9, 0, 3))); + } + + @Test + public void otherTypesUnchangedAcrossVersions() { + for (IndexType type : new IndexType[] {IndexType.STRING, IndexType.GEO2DSPHERE}) { + assertSame(type, AerospikeClient.resolveIndexType(type, version(8, 1, 2, 0))); + assertSame(type, AerospikeClient.resolveIndexType(type, version(8, 1, 3, 0))); + assertSame(type, AerospikeClient.resolveIndexType(type, version(9, 0, 0, 0))); + } + } +} diff --git a/test/src/com/aerospike/test/SuiteSync.java b/test/src/com/aerospike/test/SuiteSync.java index 9b4c9e272..87a3a06c6 100644 --- a/test/src/com/aerospike/test/SuiteSync.java +++ b/test/src/com/aerospike/test/SuiteSync.java @@ -73,6 +73,7 @@ import com.aerospike.test.sync.query.TestQueryString; import com.aerospike.test.sync.query.TestQuerySum; import com.aerospike.test.util.Args; +import com.aerospike.client.AerospikeClientIndexTypeTest; @RunWith(Suite.class) @Suite.SuiteClasses({ @@ -121,7 +122,8 @@ TestQueryOperations.class, TestQueryRPS.class, TestQueryString.class, - TestQuerySum.class + TestQuerySum.class, + AerospikeClientIndexTypeTest.class }) public class SuiteSync { public static IAerospikeClient client = null; diff --git a/test/src/com/aerospike/test/sync/query/TestIndex.java b/test/src/com/aerospike/test/sync/query/TestIndex.java index 21fdab076..4f55386f0 100644 --- a/test/src/com/aerospike/test/sync/query/TestIndex.java +++ b/test/src/com/aerospike/test/sync/query/TestIndex.java @@ -22,7 +22,9 @@ import org.junit.Test; import com.aerospike.client.AerospikeException; +import com.aerospike.client.Bin; import com.aerospike.client.Info; +import com.aerospike.client.Key; import com.aerospike.client.ResultCode; import com.aerospike.client.Value; import com.aerospike.client.cdt.CTX; @@ -30,7 +32,10 @@ import com.aerospike.client.exp.Exp; import com.aerospike.client.exp.Expression; import com.aerospike.client.exp.LoopVarPart; +import com.aerospike.client.query.Filter; import com.aerospike.client.query.IndexType; +import com.aerospike.client.query.RecordSet; +import com.aerospike.client.query.Statement; import com.aerospike.client.task.IndexTask; import com.aerospike.client.util.Version; import com.aerospike.test.sync.TestSync; @@ -39,6 +44,12 @@ public class TestIndex extends TestSync { private static final String indexName = "testindex"; private static final String binName = "testbin"; private static final String setIndexName = "testsetindex"; + private static final String integerIndexName = "testintegerindex"; + private static final String integerBinName = "testintegerbin"; + private static final String integerKeyPrefix = "testintegerkey"; + private static final String numericIndexName = "testnumericindex"; + private static final String numericBinName = "testnumericbin"; + private static final String numericKeyPrefix = "testnumerickey"; @Test public void createDrop() { @@ -116,6 +127,140 @@ public void setIndexCreateDrop() { } } + @Test + public void integerIndexCreateQueryDrop() { + Assume.assumeTrue("INTEGER index type requires server version 8.1.3 or later", + args.serverVersion.isGreaterOrEqual(8, 1, 3, 0)); + + IndexTask task; + + // Drop index if it already exists. + try { + task = client.dropIndex(args.indexPolicy, args.namespace, args.set, integerIndexName); + task.waitTillComplete(); + } + catch (AerospikeException ae) { + if (ae.getResultCode() != ResultCode.INDEX_NOTFOUND) { + throw ae; + } + } + + task = client.createIndex(args.indexPolicy, args.namespace, args.set, integerIndexName, integerBinName, IndexType.INTEGER); + task.waitTillComplete(); + + int size = 20; + + for (int i = 1; i <= size; i++) { + Key key = new Key(args.namespace, args.set, integerKeyPrefix + i); + Bin bin = new Bin(integerBinName, i); + client.put(null, key, bin); + } + + Statement stmt = new Statement(); + stmt.setNamespace(args.namespace); + stmt.setSetName(args.set); + stmt.setBinNames(integerBinName); + stmt.setFilter(Filter.range(integerBinName, 4, 8)); + + RecordSet rs = client.query(null, stmt); + + try { + int count = 0; + + while (rs.next()) { + count++; + } + assertEquals(5, count); + } + finally { + rs.close(); + } + + task = client.dropIndex(args.indexPolicy, args.namespace, args.set, integerIndexName); + task.waitTillComplete(); + + // Ensure all nodes have dropped the index. + Node[] nodes = client.getNodes(); + + for (Node node : nodes) { + String cmd = IndexTask.buildStatusCommand(args.namespace, integerIndexName, node.serverVersion); + String response = Info.request(node, cmd); + int code = Info.parseResultCode(response); + + assertEquals(201, code); + } + } + + @Test + public void numericIndexUpgradesToIntegerQueryDrop() { + // On server versions 8.1.3+ the client transparently upgrades a NUMERIC + // request to the "integer" index type. The server collapses "numeric" and + // "integer" to the same internal type, so the create spelling cannot be + // read back; this test instead verifies the upgraded index is created and + // remains queryable end-to-end. The wire-level mapping itself is asserted + // by AerospikeClientIndexTypeTest. + Assume.assumeTrue("NUMERIC to INTEGER upgrade requires server version 8.1.3 or later", + args.serverVersion.isGreaterOrEqual(8, 1, 3, 0)); + + IndexTask task; + + // Drop index if it already exists. + try { + task = client.dropIndex(args.indexPolicy, args.namespace, args.set, numericIndexName); + task.waitTillComplete(); + } + catch (AerospikeException ae) { + if (ae.getResultCode() != ResultCode.INDEX_NOTFOUND) { + throw ae; + } + } + + task = client.createIndex(args.indexPolicy, args.namespace, args.set, numericIndexName, numericBinName, IndexType.NUMERIC); + task.waitTillComplete(); + + int size = 20; + + for (int i = 1; i <= size; i++) { + Key key = new Key(args.namespace, args.set, numericKeyPrefix + i); + Bin bin = new Bin(numericBinName, i); + client.put(null, key, bin); + } + + Statement stmt = new Statement(); + stmt.setNamespace(args.namespace); + stmt.setSetName(args.set); + stmt.setBinNames(numericBinName); + stmt.setFilter(Filter.range(numericBinName, 4, 8)); + + RecordSet rs = client.query(null, stmt); + + try { + int count = 0; + + while (rs.next()) { + count++; + } + assertEquals(5, count); + } + finally { + rs.close(); + } + + task = client.dropIndex(args.indexPolicy, args.namespace, args.set, numericIndexName); + task.waitTillComplete(); + + // Ensure all nodes have dropped the index. + Node[] nodes = client.getNodes(); + + for (Node node : nodes) { + String cmd = IndexTask.buildStatusCommand(args.namespace, numericIndexName, node.serverVersion); + String response = Info.request(node, cmd); + int code = Info.parseResultCode(response); + + assertEquals(201, code); + } + } + @Test public void ctxRestore() { CTX[] ctx1 = new CTX[] {