From 45e4b39102f8f27890fcf5b838b2e1952a336534 Mon Sep 17 00:00:00 2001 From: nicktorwald Date: Thu, 19 Sep 2019 01:09:16 +0700 Subject: [PATCH 1/2] dsl: introduce Tarantool client DSL This DSL includes set of request builders that can be used as a public API to construct requests. The main idea here is to provide more natural DSL-like approach to build operations instead of current abstract types like List or List. Closes: #212 --- README.md | 188 +++++++++++++++++ .../org/tarantool/AbstractTarantoolOps.java | 192 +++++++----------- .../org/tarantool/TarantoolClientOps.java | 4 + .../tarantool/dsl/AbstractRequestSpec.java | 46 +++++ .../org/tarantool/dsl/CallRequestSpec.java | 63 ++++++ .../org/tarantool/dsl/DeleteRequestSpec.java | 66 ++++++ .../org/tarantool/dsl/EvalRequestSpec.java | 54 +++++ .../org/tarantool/dsl/ExecuteRequestSpec.java | 92 +++++++++ .../dsl/InsertOrReplaceRequestSpec.java | 81 ++++++++ .../java/org/tarantool/dsl/Operation.java | 65 ++++++ .../java/org/tarantool/dsl/Operations.java | 84 ++++++++ src/main/java/org/tarantool/dsl/Operator.java | 33 +++ .../org/tarantool/dsl/PingRequestSpec.java | 11 + src/main/java/org/tarantool/dsl/Requests.java | 109 ++++++++++ .../org/tarantool/dsl/SelectRequestSpec.java | 111 ++++++++++ .../org/tarantool/dsl/SpaceRequestSpec.java | 46 +++++ .../tarantool/dsl/TarantoolRequestSpec.java | 21 ++ .../org/tarantool/dsl/UpdateRequestSpec.java | 73 +++++++ .../org/tarantool/dsl/UpsertRequestSpec.java | 88 ++++++++ .../tarantool/ClientAsyncOperationsIT.java | 6 + 20 files changed, 1319 insertions(+), 114 deletions(-) create mode 100644 src/main/java/org/tarantool/dsl/AbstractRequestSpec.java create mode 100644 src/main/java/org/tarantool/dsl/CallRequestSpec.java create mode 100644 src/main/java/org/tarantool/dsl/DeleteRequestSpec.java create mode 100644 src/main/java/org/tarantool/dsl/EvalRequestSpec.java create mode 100644 src/main/java/org/tarantool/dsl/ExecuteRequestSpec.java create mode 100644 src/main/java/org/tarantool/dsl/InsertOrReplaceRequestSpec.java create mode 100644 src/main/java/org/tarantool/dsl/Operation.java create mode 100644 src/main/java/org/tarantool/dsl/Operations.java create mode 100644 src/main/java/org/tarantool/dsl/Operator.java create mode 100644 src/main/java/org/tarantool/dsl/PingRequestSpec.java create mode 100644 src/main/java/org/tarantool/dsl/Requests.java create mode 100644 src/main/java/org/tarantool/dsl/SelectRequestSpec.java create mode 100644 src/main/java/org/tarantool/dsl/SpaceRequestSpec.java create mode 100644 src/main/java/org/tarantool/dsl/TarantoolRequestSpec.java create mode 100644 src/main/java/org/tarantool/dsl/UpdateRequestSpec.java create mode 100644 src/main/java/org/tarantool/dsl/UpsertRequestSpec.java diff --git a/README.md b/README.md index b1e90500..659aa410 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,194 @@ all the results, you could override this: protected void complete(TarantoolPacket packet, CompletableFuture future); ``` +### Supported operation types + +Given a tarantool space as: + +```lua +box.schema.space.create('cars', { format = + { {name='id', type='integer'}," + {name='name', type='string'}," + {name='max_mph', type='integer'} } +}); +box.space.cars:create_index('pk', { type='TREE', parts={'id'} }); +box.space.cars:create_index('speed_idx', { type='TREE', unique=false, parts={'max_mph', type='unsigned'} }); +``` + +and a stored function as well: + +```lua +function getVehiclesSlowerThan(max_mph, max_size) + return box.space.cars.index.speed_idx:select(max_mph, {iterator='LT', limit=max_size}); +end; +``` + +Let's have a look what sort of operations we can apply to it using a connector. +*Note*: assume Tarantool generated id equal `512` for the newly created `cars` space. + +* SELECT (find tuples matching the search pattern) + +For instance, we can get a single tuple by id like + +```java +ops.select(512, 0, Collections.singletonList(1), 0, 1, Iterator.EQ); +``` + +or using more readable lookup names + +```java +ops.select("cars", "pk", Collections.singletonList(1), 0, 1, Iterator.EQ); +``` + +or even using builder-way to construct a query part-by-part + +```java +import static org.tarantool.dsl.Requests.selectRequest; + +ops.execute( + selectRequest("cars", "pk") + .iterator(Iterator.EQ) + .limit(1) +); +``` + +* INSERT (put a tuple in the space) + +Let's insert a new tuple into the space + +```java +ops.insert(512, Arrays.asList(1, "Lada Niva", 81)); +``` + +do the same using names + +```java +ops.insert("cars", Arrays.asList(1, "Lada Niva", 81)); +``` + +or using DSL + +```java +import static org.tarantool.dsl.Requests.insertRequest; + +ops.execute( + insertRequest("cars", Arrays.asList(1, "Lada Niva", 81)) +); +``` + +* REPLACE (insert a tuple into the space or replace an existing one) + +The syntax is quite similar to insert operation + +```java +import static org.tarantool.dsl.Requests.replaceRequest; + +ops.replace(512, Arrays.asList(2, "UAZ-469", 60)); +ops.replace("cars", Arrays.asList(2, "UAZ-469", 60)); +ops.execute( + replaceRequest("cars", Arrays.asList(2, "UAZ-469", 60)) +); +``` + +* UPDATE (update a tuple) + +Let's modify one of existing tuples + +```java +ops.update(512, Collections.singletonList(1), Arrays.asList("=", 1, "Lada 4×4")); +``` + +Lookup way: + +```java +ops.update("cars", Collections.singletonList(1), Arrays.asList("=", 1, "Lada 4×4")); +``` + +or using DSL + +```java +import static org.tarantool.dsl.Operations.assign; +import static org.tarantool.dsl.Requests.updateRequest; + +ops.execute( + updateRequest("cars", Collections.singletonList(1), assign(1, "Lada 4×4")) +); +``` + +*Note*: since Tarantool 2.3.x you can refer to tuple fields by name: + +```java +ops.update(512, Collections.singletonList(1), Arrays.asList("=", "name", "Lada 4×4")); +``` + +* UPSERT (update a tuple if it exists, otherwise try to insert it as a new tuple) + +An example looks as a mix of both insert and update operations: + +```java +import static org.tarantool.dsl.Operations.assign; +import static org.tarantool.dsl.Requests.upsertRequest; + +ops.upsert(512, Collections.singletonList(3), Arrays.asList(3, "KAMAZ-65224", 65), Arrays.asList("=", 2, 65)); +ops.upsert("cars", Collections.singletonList(3), Arrays.asList(3, "KAMAZ-65224", 65), Arrays.asList("=", 2, 65)); +ops.execute( + upsertRequest("cars", Collections.singletonList(3), assign(2, 65)) +); +``` + +*Note*: since Tarantool 2.3.x you can refer to tuple fields by name: + +```java +ops.upsert("cars", Collections.singletonList(3), Arrays.asList(3, "KAMAZ-65224", 65), Arrays.asList("=", "max_mph", 65)); +``` + +* DELETE (delete a tuple) + +Remove a tuple using one of the following forms: + +```java +import static org.tarantool.dsl.Requests.deleteRequest; + +ops().delete(512, Collections.singletonList(1)); +// same via lookup +ops().delete("cars", Collections.singletonList(1)); +// same via DSL +ops.execute(deleteRequest("cars", Collections.singletonList(1))); +``` + +* CALL / CALL v1.6 (call a stored function) + +Let's invoke the predefined function to fetch slower enough vehicles: + +```java +import static org.tarantool.dsl.Requests.callRequest; + +ops().call("getVehiclesSlowerThan", 80, 10); +// same via DSL +ops.execute(callRequest("getVehiclesSlowerThan").arguments(80, 10)); +``` + +*NOTE*: to use obsolete Tarantool v1.6 operation, configure it as follows: + +```java +ops().setCallCode(Code.OLD_CALL); +ops().call("getVehiclesSlowerThan", 80, 10); +// same via DSL +ops.execute(callRequest("getVehiclesSlowerThan").arguments(80, 10).useCall16(true)); +``` \ + +* EVAL (evaluate a Lua expression) + +To evaluate expressions using Lua, you can invoke the following operation: + +```java +import static org.tarantool.dsl.Requests.evalRequest; + +ops().eval("return getVehiclesSlowerThan(...)", 90, 50); +// same via DSL +ops.execute(evalRequest("return getVehiclesSlowerThan(...)")).arguments(90, 50)); +``` + ### Client config options The client configuration options are represented through the `TarantoolClientConfig` class. diff --git a/src/main/java/org/tarantool/AbstractTarantoolOps.java b/src/main/java/org/tarantool/AbstractTarantoolOps.java index bdef668d..a61017c5 100644 --- a/src/main/java/org/tarantool/AbstractTarantoolOps.java +++ b/src/main/java/org/tarantool/AbstractTarantoolOps.java @@ -1,15 +1,29 @@ package org.tarantool; -import static org.tarantool.TarantoolRequestArgumentFactory.cacheLookupValue; -import static org.tarantool.TarantoolRequestArgumentFactory.value; - +import static org.tarantool.dsl.Requests.callRequest; +import static org.tarantool.dsl.Requests.deleteRequest; +import static org.tarantool.dsl.Requests.evalRequest; +import static org.tarantool.dsl.Requests.insertRequest; +import static org.tarantool.dsl.Requests.pingRequest; +import static org.tarantool.dsl.Requests.replaceRequest; +import static org.tarantool.dsl.Requests.selectRequest; +import static org.tarantool.dsl.Requests.updateRequest; +import static org.tarantool.dsl.Requests.upsertRequest; + +import org.tarantool.dsl.Operation; +import org.tarantool.dsl.TarantoolRequestSpec; +import org.tarantool.logging.Logger; +import org.tarantool.logging.LoggerFactory; import org.tarantool.schema.TarantoolSchemaMeta; +import java.util.Arrays; import java.util.List; public abstract class AbstractTarantoolOps implements TarantoolClientOps, Object, Result> { + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractTarantoolOps.class); + private Code callCode = Code.CALL; protected abstract Result exec(TarantoolRequest request); @@ -17,184 +31,134 @@ public abstract class AbstractTarantoolOps protected abstract TarantoolSchemaMeta getSchemaMeta(); public Result select(Integer space, Integer index, List key, int offset, int limit, Iterator iterator) { - return select(space, index, key, offset, limit, iterator.getValue()); + return execute( + selectRequest(space, index) + .key(key) + .offset(offset).limit(limit) + .iterator(iterator) + ); } @Override public Result select(String space, String index, List key, int offset, int limit, Iterator iterator) { - return select(space, index, key, offset, limit, iterator.getValue()); + return execute( + selectRequest(space, index) + .key(key) + .offset(offset).limit(limit) + .iterator(iterator) + ); } @Override public Result select(Integer space, Integer index, List key, int offset, int limit, int iterator) { - return exec( - new TarantoolRequest( - Code.SELECT, - value(Key.SPACE), value(space), - value(Key.INDEX), value(index), - value(Key.KEY), value(key), - value(Key.ITERATOR), value(iterator), - value(Key.LIMIT), value(limit), - value(Key.OFFSET), value(offset) - ) + return execute( + selectRequest(space, index) + .key(key) + .offset(offset).limit(limit) + .iterator(iterator) ); } @Override public Result select(String space, String index, List key, int offset, int limit, int iterator) { - return exec( - new TarantoolRequest( - Code.SELECT, - value(Key.SPACE), cacheLookupValue(() -> getSchemaMeta().getSpace(space).getId()), - value(Key.INDEX), cacheLookupValue(() -> getSchemaMeta().getSpaceIndex(space, index).getId()), - value(Key.KEY), value(key), - value(Key.ITERATOR), value(iterator), - value(Key.LIMIT), value(limit), - value(Key.OFFSET), value(offset) - ) + return execute( + selectRequest(space, index) + .key(key) + .offset(offset).limit(limit) + .iterator(iterator) ); } @Override public Result insert(Integer space, List tuple) { - return exec(new TarantoolRequest( - Code.INSERT, - value(Key.SPACE), value(space), - value(Key.TUPLE), value(tuple) - ) - ); + return execute(insertRequest(space, tuple)); } @Override public Result insert(String space, List tuple) { - return exec( - new TarantoolRequest( - Code.INSERT, - value(Key.SPACE), cacheLookupValue(() -> getSchemaMeta().getSpace(space).getId()), - value(Key.TUPLE), value(tuple) - ) - ); + return execute(insertRequest(space, tuple)); } @Override public Result replace(Integer space, List tuple) { - return exec( - new TarantoolRequest( - Code.REPLACE, - value(Key.SPACE), value(space), - value(Key.TUPLE), value(tuple) - ) - ); + return execute(replaceRequest(space, tuple)); } @Override public Result replace(String space, List tuple) { - return exec( - new TarantoolRequest( - Code.REPLACE, - value(Key.SPACE), cacheLookupValue(() -> getSchemaMeta().getSpace(space).getId()), - value(Key.TUPLE), value(tuple) - ) - ); + return execute(replaceRequest(space, tuple)); } @Override public Result update(Integer space, List key, Object... operations) { - return exec( - new TarantoolRequest( - Code.UPDATE, - value(Key.SPACE), value(space), - value(Key.KEY), value(key), - value(Key.TUPLE), value(operations) - ) - ); + Operation[] ops = Arrays.stream(operations) + .map(Operation::fromArray) + .toArray(org.tarantool.dsl.Operation[]::new); + return execute(updateRequest(space, key, ops)); } @Override public Result update(String space, List key, Object... operations) { - return exec( - new TarantoolRequest( - Code.UPDATE, - value(Key.SPACE), cacheLookupValue(() -> getSchemaMeta().getSpace(space).getId()), - value(Key.KEY), value(key), - value(Key.TUPLE), value(operations) - ) - ); + Operation[] ops = Arrays.stream(operations) + .map(Operation::fromArray) + .toArray(org.tarantool.dsl.Operation[]::new); + return execute(updateRequest(space, key, ops)); } @Override public Result upsert(Integer space, List key, List defTuple, Object... operations) { - return exec( - new TarantoolRequest( - Code.UPSERT, - value(Key.SPACE), value(space), - value(Key.KEY), value(key), - value(Key.TUPLE), value(defTuple), - value(Key.UPSERT_OPS), value(operations) - ) - ); + Operation[] ops = Arrays.stream(operations) + .map(Operation::fromArray) + .toArray(Operation[]::new); + return execute(upsertRequest(space, key, defTuple, ops)); } @Override public Result upsert(String space, List key, List defTuple, Object... operations) { - return exec( - new TarantoolRequest( - Code.UPSERT, - value(Key.SPACE), cacheLookupValue(() -> getSchemaMeta().getSpace(space).getId()), - value(Key.KEY), value(key), - value(Key.TUPLE), value(defTuple), - value(Key.UPSERT_OPS), value(operations) - ) - ); + Operation[] ops = Arrays.stream(operations) + .map(Operation::fromArray) + .toArray(Operation[]::new); + return execute(upsertRequest(space, key, defTuple, ops)); } @Override public Result delete(Integer space, List key) { - return exec( - new TarantoolRequest( - Code.DELETE, - value(Key.SPACE), value(space), - value(Key.KEY), value(key) - ) - ); + return execute(deleteRequest(space, key)); } @Override public Result delete(String space, List key) { - return exec( - new TarantoolRequest( - Code.DELETE, - value(Key.SPACE), cacheLookupValue(() -> getSchemaMeta().getSpace(space).getId()), - value(Key.KEY), value(key) - ) - ); + return execute(deleteRequest(space, key)); } @Override public Result call(String function, Object... args) { - return exec( - new TarantoolRequest( - callCode, - value(Key.FUNCTION), value(function), - value(Key.TUPLE), value(args) - ) + return execute( + callRequest(function) + .arguments(args) + .useCall16(callCode == Code.OLD_CALL) ); } @Override public Result eval(String expression, Object... args) { - return exec( - new TarantoolRequest( - Code.EVAL, - value(Key.EXPRESSION), value(expression), - value(Key.TUPLE), value(args) - ) - ); + return execute(evalRequest(expression).arguments(args)); } @Override public void ping() { - exec(new TarantoolRequest(Code.PING)); + execute(pingRequest()); + } + + @Override + public Result execute(TarantoolRequestSpec requestSpec) { + TarantoolSchemaMeta schemaMeta = null; + try { + schemaMeta = getSchemaMeta(); + } catch (Exception cause) { + LOGGER.warn(() -> "Could not get Tarantool schema meta-info", cause); + } + return exec(requestSpec.toTarantoolRequest(schemaMeta)); } public void setCallCode(Code callCode) { diff --git a/src/main/java/org/tarantool/TarantoolClientOps.java b/src/main/java/org/tarantool/TarantoolClientOps.java index 9a466c44..9f4a00de 100644 --- a/src/main/java/org/tarantool/TarantoolClientOps.java +++ b/src/main/java/org/tarantool/TarantoolClientOps.java @@ -1,5 +1,7 @@ package org.tarantool; +import org.tarantool.dsl.TarantoolRequestSpec; + /** * Provides a set of typical operations with data in Tarantool. * @@ -41,6 +43,8 @@ public interface TarantoolClientOps { R eval(String expression, Object... args); + R execute(TarantoolRequestSpec requestSpec); + void ping(); void close(); diff --git a/src/main/java/org/tarantool/dsl/AbstractRequestSpec.java b/src/main/java/org/tarantool/dsl/AbstractRequestSpec.java new file mode 100644 index 00000000..877bb379 --- /dev/null +++ b/src/main/java/org/tarantool/dsl/AbstractRequestSpec.java @@ -0,0 +1,46 @@ +package org.tarantool.dsl; + +import org.tarantool.Code; +import org.tarantool.TarantoolRequest; +import org.tarantool.schema.TarantoolSchemaMeta; + +import java.time.Duration; + +public abstract class AbstractRequestSpec> + implements TarantoolRequestSpec { + + final Code code; + Duration duration = Duration.ZERO; + boolean useDefaultTimeout = true; + + AbstractRequestSpec(Code code) { + this.code = code; + } + + AbstractRequestSpec(Code code, Duration duration) { + this.code = code; + this.duration = duration; + } + + @SuppressWarnings("unchecked") + public B timeout(Duration duration) { + this.duration = duration; + this.useDefaultTimeout = false; + return (B) this; + } + + @SuppressWarnings("unchecked") + public B useDefaultTimeout() { + this.duration = Duration.ZERO; + this.useDefaultTimeout = true; + return (B) this; + } + + @Override + public TarantoolRequest toTarantoolRequest(TarantoolSchemaMeta schemaMeta) { + TarantoolRequest request = new TarantoolRequest(code); + request.setTimeout(useDefaultTimeout ? null : duration); + return request; + } + +} diff --git a/src/main/java/org/tarantool/dsl/CallRequestSpec.java b/src/main/java/org/tarantool/dsl/CallRequestSpec.java new file mode 100644 index 00000000..741e85d3 --- /dev/null +++ b/src/main/java/org/tarantool/dsl/CallRequestSpec.java @@ -0,0 +1,63 @@ +package org.tarantool.dsl; + +import static org.tarantool.TarantoolRequestArgumentFactory.value; + +import org.tarantool.Code; +import org.tarantool.Key; +import org.tarantool.TarantoolRequest; +import org.tarantool.schema.TarantoolSchemaMeta; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class CallRequestSpec extends AbstractRequestSpec { + + private String functionName; + private List arguments = new ArrayList<>(); + private boolean useCall16 = false; + + CallRequestSpec(String functionName) { + super(Code.CALL); + this.functionName = Objects.requireNonNull(functionName); + } + + public CallRequestSpec function(String functionName) { + Objects.requireNonNull(functionName); + this.functionName = functionName; + return this; + } + + public CallRequestSpec arguments(Object... arguments) { + this.arguments.clear(); + Collections.addAll(this.arguments, arguments); + return this; + } + + public CallRequestSpec arguments(Collection arguments) { + this.arguments.clear(); + this.arguments.addAll(arguments); + return this; + } + + public CallRequestSpec useCall16(boolean flag) { + this.useCall16 = flag; + return this; + } + + @Override + public TarantoolRequest toTarantoolRequest(TarantoolSchemaMeta schemaMeta) { + TarantoolRequest request = super.toTarantoolRequest(schemaMeta); + if (useCall16) { + request.setCode(Code.OLD_CALL); + } + request.addArguments( + value(Key.FUNCTION), value(functionName), + value(Key.TUPLE), value(arguments) + ); + return request; + } + +} diff --git a/src/main/java/org/tarantool/dsl/DeleteRequestSpec.java b/src/main/java/org/tarantool/dsl/DeleteRequestSpec.java new file mode 100644 index 00000000..78de41f3 --- /dev/null +++ b/src/main/java/org/tarantool/dsl/DeleteRequestSpec.java @@ -0,0 +1,66 @@ +package org.tarantool.dsl; + +import static org.tarantool.TarantoolRequestArgumentFactory.cacheLookupValue; +import static org.tarantool.TarantoolRequestArgumentFactory.value; + +import org.tarantool.Code; +import org.tarantool.Key; +import org.tarantool.TarantoolRequest; +import org.tarantool.schema.TarantoolSchemaMeta; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class DeleteRequestSpec extends SpaceRequestSpec { + + private List key; + + DeleteRequestSpec(int spaceId, List key) { + super(Code.DELETE, spaceId); + this.key = new ArrayList<>(key); + } + + DeleteRequestSpec(int spaceId, Object... keyParts) { + super(Code.DELETE, spaceId); + this.key = Arrays.asList(keyParts); + } + + DeleteRequestSpec(String spaceName, List key) { + super(Code.DELETE, spaceName); + this.key = new ArrayList<>(key); + } + + DeleteRequestSpec(String spaceName, Object... keyParts) { + super(Code.DELETE, spaceName); + this.key = Arrays.asList(keyParts); + } + + public DeleteRequestSpec primaryKey(Object... keyParts) { + this.key.clear(); + Collections.addAll(this.key, keyParts); + return this; + } + + public DeleteRequestSpec primaryKey(Collection key) { + this.key.clear(); + this.key.addAll(key); + return this; + } + + @Override + public TarantoolRequest toTarantoolRequest(TarantoolSchemaMeta schemaMeta) { + TarantoolRequest request = super.toTarantoolRequest(schemaMeta); + request.addArguments( + value(Key.SPACE), + spaceId == null + ? cacheLookupValue(() -> schemaMeta.getSpace(spaceName).getId()) + : value(spaceId), + value(Key.KEY), value(key) + ); + return request; + } + +} diff --git a/src/main/java/org/tarantool/dsl/EvalRequestSpec.java b/src/main/java/org/tarantool/dsl/EvalRequestSpec.java new file mode 100644 index 00000000..b5a080b3 --- /dev/null +++ b/src/main/java/org/tarantool/dsl/EvalRequestSpec.java @@ -0,0 +1,54 @@ +package org.tarantool.dsl; + +import static org.tarantool.TarantoolRequestArgumentFactory.value; + +import org.tarantool.Code; +import org.tarantool.Key; +import org.tarantool.TarantoolRequest; +import org.tarantool.schema.TarantoolSchemaMeta; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class EvalRequestSpec extends AbstractRequestSpec { + + private String expression; + private List arguments = new ArrayList<>(); + + EvalRequestSpec(String expression) { + super(Code.EVAL); + this.expression = Objects.requireNonNull(expression); + } + + public EvalRequestSpec expression(String expression) { + Objects.requireNonNull(expression); + this.expression = expression; + return this; + } + + public EvalRequestSpec arguments(Object... arguments) { + this.arguments.clear(); + Collections.addAll(this.arguments, arguments); + return this; + } + + public EvalRequestSpec arguments(Collection arguments) { + this.arguments.clear(); + this.arguments.addAll(arguments); + return this; + } + + @Override + public TarantoolRequest toTarantoolRequest(TarantoolSchemaMeta schemaMeta) { + TarantoolRequest request = super.toTarantoolRequest(schemaMeta); + request.addArguments( + value(Key.EXPRESSION), value(expression), + value(Key.TUPLE), value(arguments) + ); + return request; + } + +} diff --git a/src/main/java/org/tarantool/dsl/ExecuteRequestSpec.java b/src/main/java/org/tarantool/dsl/ExecuteRequestSpec.java new file mode 100644 index 00000000..c54ad34c --- /dev/null +++ b/src/main/java/org/tarantool/dsl/ExecuteRequestSpec.java @@ -0,0 +1,92 @@ +package org.tarantool.dsl; + +import static org.tarantool.TarantoolRequestArgumentFactory.value; + +import org.tarantool.Code; +import org.tarantool.Key; +import org.tarantool.TarantoolRequest; +import org.tarantool.schema.TarantoolSchemaMeta; +import org.tarantool.util.TupleTwo; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class ExecuteRequestSpec extends AbstractRequestSpec { + + private String sqlText; + private List ordinalBindings = new ArrayList<>(); + private List> namedBindings = new ArrayList<>(); + + ExecuteRequestSpec(String sqlText) { + super(Code.EXECUTE); + this.sqlText = Objects.requireNonNull(sqlText); + } + + public ExecuteRequestSpec sql(String text) { + Objects.requireNonNull(text); + this.sqlText = text; + return this; + } + + public ExecuteRequestSpec ordinalParameters(Object... bindings) { + this.ordinalBindings.clear(); + Collections.addAll(this.ordinalBindings, bindings); + this.namedBindings.clear(); + return this; + } + + public ExecuteRequestSpec ordinalParameters(Collection bindings) { + this.ordinalBindings.clear(); + this.ordinalBindings.addAll(bindings); + this.namedBindings.clear(); + return this; + } + + public ExecuteRequestSpec namedParameters(Map bindings) { + this.namedBindings.clear(); + this.namedBindings.addAll( + bindings.entrySet().stream() + .map(e -> TupleTwo.of(e.getKey(), e.getValue())) + .collect(Collectors.toList()) + ); + this.ordinalBindings.clear(); + return this; + } + + public ExecuteRequestSpec namedParameters(TupleTwo[] bindings) { + this.namedBindings.clear(); + for (TupleTwo binding : bindings) { + this.namedBindings.add(TupleTwo.of(binding.getFirst(), binding.getSecond())); + } + this.ordinalBindings.clear(); + return this; + } + + @Override + public TarantoolRequest toTarantoolRequest(TarantoolSchemaMeta schemaMeta) { + TarantoolRequest request = super.toTarantoolRequest(schemaMeta); + request.addArguments( + value(Key.SQL_TEXT), + value(sqlText) + ); + if (!ordinalBindings.isEmpty()) { + request.addArguments( + value(Key.SQL_BIND), + value(ordinalBindings) + ); + } + if (!namedBindings.isEmpty()) { + request.addArguments( + value(Key.SQL_BIND), + value(namedBindings) + ); + } + return request; + } + +} diff --git a/src/main/java/org/tarantool/dsl/InsertOrReplaceRequestSpec.java b/src/main/java/org/tarantool/dsl/InsertOrReplaceRequestSpec.java new file mode 100644 index 00000000..03890072 --- /dev/null +++ b/src/main/java/org/tarantool/dsl/InsertOrReplaceRequestSpec.java @@ -0,0 +1,81 @@ +package org.tarantool.dsl; + +import static org.tarantool.TarantoolRequestArgumentFactory.cacheLookupValue; +import static org.tarantool.TarantoolRequestArgumentFactory.value; + +import org.tarantool.Code; +import org.tarantool.Key; +import org.tarantool.TarantoolRequest; +import org.tarantool.schema.TarantoolSchemaMeta; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class InsertOrReplaceRequestSpec extends SpaceRequestSpec { + + public enum Mode { + INSERT(Code.INSERT), + REPLACE(Code.REPLACE); + + final Code code; + + Mode(Code code) { + this.code = code; + } + + public Code getCode() { + return code; + } + } + + private List tuple; + + InsertOrReplaceRequestSpec(Mode mode, int spaceId, List tuple) { + super(mode.getCode(), spaceId); + this.tuple = new ArrayList<>(tuple); + } + + InsertOrReplaceRequestSpec(Mode mode, String spaceName, List tuple) { + super(mode.getCode(), spaceName); + this.tuple = new ArrayList<>(tuple); + } + + InsertOrReplaceRequestSpec(Mode mode, int spaceId, Object... tupleItems) { + super(mode.getCode(), spaceId); + this.tuple = Arrays.asList(tupleItems); + } + + InsertOrReplaceRequestSpec(Mode mode, String spaceName, Object... tupleItems) { + super(mode.getCode(), spaceName); + this.tuple = Arrays.asList(tupleItems); + } + + public InsertOrReplaceRequestSpec tuple(Object... tupleItems) { + this.tuple.clear(); + Collections.addAll(this.tuple, tupleItems); + return this; + } + + public InsertOrReplaceRequestSpec tuple(Collection tuple) { + this.tuple.clear(); + this.tuple.addAll(tuple); + return this; + } + + @Override + public TarantoolRequest toTarantoolRequest(TarantoolSchemaMeta schemaMeta) { + TarantoolRequest request = super.toTarantoolRequest(schemaMeta); + request.addArguments( + value(Key.SPACE), + spaceId == null + ? cacheLookupValue(() -> schemaMeta.getSpace(spaceName).getId()) + : value(spaceId), + value(Key.TUPLE), value(tuple) + ); + return request; + } + +} diff --git a/src/main/java/org/tarantool/dsl/Operation.java b/src/main/java/org/tarantool/dsl/Operation.java new file mode 100644 index 00000000..aae09a6f --- /dev/null +++ b/src/main/java/org/tarantool/dsl/Operation.java @@ -0,0 +1,65 @@ +package org.tarantool.dsl; + +import java.util.Arrays; +import java.util.List; + +public class Operation { + + private final Operator operator; + private final List operands; + + public Operation(Operator operator, Object... operands) { + this.operator = operator; + this.operands = Arrays.asList(operands); + } + + public Operator getOperator() { + return operator; + } + + public Object[] toArray() { + Object[] array = new Object[operands.size() + 1]; + array[0] = operator.getOpCode(); + for (int i = 1; i < array.length; i++) { + array[i] = operands.get(i - 1); + } + return array; + } + + /** + * It's used to perform a transformation between raw type + * and type safe DSL Operation class. This is required + * because of being compatible with old operations interface + * and a new DSL approach. + * + * This client expects an operation in format of simple + * array or list like {opCode, args...}. For instance, + * addition 3 to second field will be {"+", 2, 3} + * + * @param operation raw operation + * + * @return type safe operation + */ + public static Operation fromArray(Object operation) { + try { + if (operation instanceof Object[]) { + Object[] opArray = (Object[]) operation; + String code = opArray[0].toString(); + Object[] args = new Object[opArray.length - 1]; + System.arraycopy(opArray, 1, args, 0, args.length); + return new Operation(Operator.byOpCode(code), args); + } + List opList = (List) operation; + String code = opList.get(0).toString(); + Object[] args = opList.subList(1, opList.size()).toArray(); + return new Operation(Operator.byOpCode(code), args); + } catch (Exception cause) { + throw new IllegalArgumentException( + "Operation is invalid. Use an array or list as {\"opCode\", args...}. " + + "Or use request DSL to build type safe operation.", + cause + ); + } + } + +} diff --git a/src/main/java/org/tarantool/dsl/Operations.java b/src/main/java/org/tarantool/dsl/Operations.java new file mode 100644 index 00000000..e8ae5b1c --- /dev/null +++ b/src/main/java/org/tarantool/dsl/Operations.java @@ -0,0 +1,84 @@ +package org.tarantool.dsl; + +import java.util.Objects; + +public class Operations { + + public static Operation add(int fieldNumber, long value) { + return new Operation(Operator.ADDITION, fieldNumber, value); + } + + public static Operation add(String fieldName, long value) { + return new Operation(Operator.ADDITION, fieldName, value); + } + + public static Operation subtract(int fieldNumber, long value) { + return new Operation(Operator.SUBTRACTION, fieldNumber, value); + } + + public static Operation subtract(String fieldName, long value) { + return new Operation(Operator.SUBTRACTION, fieldName, value); + } + + public static Operation bitwiseAnd(int fieldNumber, long value) { + return new Operation(Operator.BITWISE_AND, fieldNumber, value); + } + + public static Operation bitwiseAnd(String fieldName, long value) { + return new Operation(Operator.BITWISE_AND, fieldName, value); + } + + public static Operation bitwiseOr(int fieldNumber, long value) { + return new Operation(Operator.BITWISE_OR, fieldNumber, value); + } + + public static Operation bitwiseOr(String fieldName, long value) { + return new Operation(Operator.BITWISE_OR, fieldName, value); + } + + public static Operation bitwiseXor(int fieldNumber, long value) { + return new Operation(Operator.BITWISE_XOR, fieldNumber, value); + } + + public static Operation bitwiseXor(String fieldName, long value) { + return new Operation(Operator.BITWISE_XOR, fieldName, value); + } + + public static Operation splice(int fieldNumber, int position, int offset, String substitution) { + return new Operation(Operator.SPLICE, fieldNumber, position, offset, substitution); + } + + public static Operation splice(String fieldName, int position, int offset, String substitution) { + return new Operation(Operator.SPLICE, fieldName, position, offset, substitution); + } + + public static Operation insert(int fieldNumber, Object value) { + return new Operation(Operator.INSERT, fieldNumber, value); + } + + public static Operation insert(String fieldName, Object value) { + return new Operation(Operator.INSERT, fieldName, value); + } + + public static Operation delete(int fromField, int length) { + return new Operation(Operator.DELETE, fromField, length); + } + + public static Operation delete(String fromField, int length) { + return new Operation(Operator.DELETE, fromField, length); + } + + public static Operation assign(int fieldNumber, Object value) { + return new Operation(Operator.ASSIGN, fieldNumber, value); + } + + public static Operation assign(String fieldName, Object value) { + return new Operation(Operator.ASSIGN, fieldName, value); + } + + private static Operation createOperation(Operator operator, int fieldNumber, Object value) { + Objects.requireNonNull(value); + return new Operation(operator, fieldNumber, value); + } + +} diff --git a/src/main/java/org/tarantool/dsl/Operator.java b/src/main/java/org/tarantool/dsl/Operator.java new file mode 100644 index 00000000..ae69e0c9 --- /dev/null +++ b/src/main/java/org/tarantool/dsl/Operator.java @@ -0,0 +1,33 @@ +package org.tarantool.dsl; + +import java.util.stream.Stream; + +public enum Operator { + ADDITION("+"), + SUBTRACTION("-"), + BITWISE_AND("&"), + BITWISE_OR("|"), + BITWISE_XOR("^"), + SPLICE(":"), + INSERT("!"), + DELETE("#"), + ASSIGN("="); + + private final String opCode; + + Operator(String opCode) { + this.opCode = opCode; + } + + public String getOpCode() { + return opCode; + } + + public static Operator byOpCode(String opCode) { + return Stream.of(Operator.values()) + .filter(s -> s.getOpCode().equals(opCode)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + } + +} diff --git a/src/main/java/org/tarantool/dsl/PingRequestSpec.java b/src/main/java/org/tarantool/dsl/PingRequestSpec.java new file mode 100644 index 00000000..3449edad --- /dev/null +++ b/src/main/java/org/tarantool/dsl/PingRequestSpec.java @@ -0,0 +1,11 @@ +package org.tarantool.dsl; + +import org.tarantool.Code; + +public class PingRequestSpec extends AbstractRequestSpec { + + PingRequestSpec() { + super(Code.PING); + } + +} diff --git a/src/main/java/org/tarantool/dsl/Requests.java b/src/main/java/org/tarantool/dsl/Requests.java new file mode 100644 index 00000000..2b0ca763 --- /dev/null +++ b/src/main/java/org/tarantool/dsl/Requests.java @@ -0,0 +1,109 @@ +package org.tarantool.dsl; + +import org.tarantool.dsl.InsertOrReplaceRequestSpec.Mode; + +import java.util.List; + +/** + * Entry point to build requests + * using DSL approach a.k.a Request DSL. + */ +public class Requests { + + public static SelectRequestSpec selectRequest(int space, int index) { + return new SelectRequestSpec(space, index); + } + + public static SelectRequestSpec selectRequest(String space, int index) { + return new SelectRequestSpec(space, index); + } + + public static SelectRequestSpec selectRequest(int space, String index) { + return new SelectRequestSpec(space, index); + } + + public static SelectRequestSpec selectRequest(String space, String index) { + return new SelectRequestSpec(space, index); + } + + public static InsertOrReplaceRequestSpec insertRequest(int space, List tuple) { + return new InsertOrReplaceRequestSpec(Mode.INSERT, space, tuple); + } + + public static InsertOrReplaceRequestSpec insertRequest(int space, Object... tupleItems) { + return new InsertOrReplaceRequestSpec(Mode.INSERT, space, tupleItems); + } + + public static InsertOrReplaceRequestSpec insertRequest(String space, List tuple) { + return new InsertOrReplaceRequestSpec(Mode.INSERT, space, tuple); + } + + public static InsertOrReplaceRequestSpec insertRequest(String space, Object... tupleItems) { + return new InsertOrReplaceRequestSpec(Mode.INSERT, space, tupleItems); + } + + public static InsertOrReplaceRequestSpec replaceRequest(int space, List tuple) { + return new InsertOrReplaceRequestSpec(Mode.REPLACE, space, tuple); + } + + public static InsertOrReplaceRequestSpec replaceRequest(int space, Object... tupleItems) { + return new InsertOrReplaceRequestSpec(Mode.REPLACE, space, tupleItems); + } + + public static InsertOrReplaceRequestSpec replaceRequest(String space, List tuple) { + return new InsertOrReplaceRequestSpec(Mode.REPLACE, space, tuple); + } + + public static InsertOrReplaceRequestSpec replaceRequest(String space, Object... tupleItems) { + return new InsertOrReplaceRequestSpec(Mode.REPLACE, space, tupleItems); + } + + public static UpdateRequestSpec updateRequest(int space, List key, Operation... operations) { + return new UpdateRequestSpec(space, key, operations); + } + + public static UpdateRequestSpec updateRequest(String space, List key, Operation... operations) { + return new UpdateRequestSpec(space, key, operations); + } + + public static UpsertRequestSpec upsertRequest(int space, List key, List tuple, Operation... operations) { + return new UpsertRequestSpec(space, key, tuple, operations); + } + + public static UpsertRequestSpec upsertRequest(String space, List key, List tuple, Operation... operations) { + return new UpsertRequestSpec(space, key, tuple, operations); + } + + public static DeleteRequestSpec deleteRequest(int space, List key) { + return new DeleteRequestSpec(space, key); + } + + public static DeleteRequestSpec deleteRequest(int space, Object... keyParts) { + return new DeleteRequestSpec(space, keyParts); + } + + public static DeleteRequestSpec deleteRequest(String space, List key) { + return new DeleteRequestSpec(space, key); + } + + public static DeleteRequestSpec deleteRequest(String space, Object... keyParts) { + return new DeleteRequestSpec(space, keyParts); + } + + public static CallRequestSpec callRequest(String function) { + return new CallRequestSpec(function); + } + + public static EvalRequestSpec evalRequest(String expression) { + return new EvalRequestSpec(expression); + } + + public static PingRequestSpec pingRequest() { + return new PingRequestSpec(); + } + + public static ExecuteRequestSpec executeRequest(String sql) { + return new ExecuteRequestSpec(sql); + } + +} diff --git a/src/main/java/org/tarantool/dsl/SelectRequestSpec.java b/src/main/java/org/tarantool/dsl/SelectRequestSpec.java new file mode 100644 index 00000000..bac73e45 --- /dev/null +++ b/src/main/java/org/tarantool/dsl/SelectRequestSpec.java @@ -0,0 +1,111 @@ +package org.tarantool.dsl; + +import static org.tarantool.TarantoolRequestArgumentFactory.cacheLookupValue; +import static org.tarantool.TarantoolRequestArgumentFactory.value; + +import org.tarantool.Code; +import org.tarantool.Iterator; +import org.tarantool.Key; +import org.tarantool.TarantoolRequest; +import org.tarantool.schema.TarantoolSchemaMeta; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class SelectRequestSpec extends SpaceRequestSpec { + + private Integer indexId; + private String indexName; + private List key = new ArrayList<>(); + private Iterator iterator = Iterator.ALL; + private int offset = 0; + private int limit = Integer.MAX_VALUE; + + public SelectRequestSpec(int spaceId, int indexId) { + super(Code.SELECT, spaceId); + this.indexId = indexId; + } + + public SelectRequestSpec(int spaceId, String indexName) { + super(Code.SELECT, spaceId); + this.indexName = Objects.requireNonNull(indexName); + } + + public SelectRequestSpec(String spaceName, int indexId) { + super(Code.SELECT, spaceName); + this.indexId = indexId; + } + + public SelectRequestSpec(String spaceName, String indexName) { + super(Code.SELECT, spaceName); + this.indexName = Objects.requireNonNull(indexName); + } + + public SelectRequestSpec index(int indexId) { + this.indexId = indexId; + this.indexName = null; + return this; + } + + public SelectRequestSpec index(String indexName) { + this.indexName = Objects.requireNonNull(indexName); + this.indexId = null; + return this; + } + + public SelectRequestSpec key(Object... keyParts) { + this.key.clear(); + Collections.addAll(this.key, keyParts); + return this; + } + + public SelectRequestSpec key(Collection key) { + this.key.clear(); + this.key.addAll(key); + return this; + } + + public SelectRequestSpec iterator(Iterator iterator) { + this.iterator = iterator; + return this; + } + + public SelectRequestSpec iterator(int iterator) { + this.iterator = Iterator.valueOf(iterator); + return this; + } + + public SelectRequestSpec offset(int offset) { + this.offset = offset; + return this; + } + + public SelectRequestSpec limit(int limit) { + this.limit = limit; + return this; + } + + @Override + public TarantoolRequest toTarantoolRequest(TarantoolSchemaMeta schemaMeta) { + TarantoolRequest request = super.toTarantoolRequest(schemaMeta); + request.addArguments( + value(Key.SPACE), + spaceId == null + ? cacheLookupValue(() -> schemaMeta.getSpace(spaceName).getId()) + : value(spaceId), + value(Key.INDEX), + indexId == null + ? cacheLookupValue(() -> schemaMeta.getSpaceIndex(spaceName, indexName).getId()) + : value(indexId), + value(Key.KEY), value(key), + value(Key.ITERATOR), value(iterator.getValue()), + value(Key.LIMIT), value(limit), + value(Key.OFFSET), value(offset) + ); + return request; + } + +} diff --git a/src/main/java/org/tarantool/dsl/SpaceRequestSpec.java b/src/main/java/org/tarantool/dsl/SpaceRequestSpec.java new file mode 100644 index 00000000..fb03152d --- /dev/null +++ b/src/main/java/org/tarantool/dsl/SpaceRequestSpec.java @@ -0,0 +1,46 @@ +package org.tarantool.dsl; + +import org.tarantool.Code; + +import java.util.Objects; + +/** + * Supports space related DSL builders. + * + * @param current build type + */ +public abstract class SpaceRequestSpec> + extends AbstractRequestSpec { + + Integer spaceId; + String spaceName; + + public SpaceRequestSpec(Code code) { + super(code); + } + + public SpaceRequestSpec(Code code, int spaceId) { + this(code); + this.spaceId = spaceId; + } + + public SpaceRequestSpec(Code code, String spaceName) { + this(code); + this.spaceName = Objects.requireNonNull(spaceName); + } + + @SuppressWarnings("unchecked") + public B space(int spaceId) { + this.spaceId = spaceId; + this.spaceName = null; + return (B) this; + } + + @SuppressWarnings("unchecked") + public B space(String spaceName) { + this.spaceName = Objects.requireNonNull(spaceName); + this.spaceId = null; + return (B) this; + } + +} diff --git a/src/main/java/org/tarantool/dsl/TarantoolRequestSpec.java b/src/main/java/org/tarantool/dsl/TarantoolRequestSpec.java new file mode 100644 index 00000000..6fb4b96e --- /dev/null +++ b/src/main/java/org/tarantool/dsl/TarantoolRequestSpec.java @@ -0,0 +1,21 @@ +package org.tarantool.dsl; + +import org.tarantool.TarantoolRequest; +import org.tarantool.schema.TarantoolSchemaMeta; + +/** + * Used to convert DSL builder to appropriate + * Tarantool requests. + * + * This interface is not a part of public API. + */ +public interface TarantoolRequestSpec { + + /** + * Converts the target to {@link TarantoolRequest}. + * + * @return converted request + */ + TarantoolRequest toTarantoolRequest(TarantoolSchemaMeta schemaMeta); + +} diff --git a/src/main/java/org/tarantool/dsl/UpdateRequestSpec.java b/src/main/java/org/tarantool/dsl/UpdateRequestSpec.java new file mode 100644 index 00000000..d2b0105b --- /dev/null +++ b/src/main/java/org/tarantool/dsl/UpdateRequestSpec.java @@ -0,0 +1,73 @@ +package org.tarantool.dsl; + +import static org.tarantool.TarantoolRequestArgumentFactory.cacheLookupValue; +import static org.tarantool.TarantoolRequestArgumentFactory.value; + +import org.tarantool.Code; +import org.tarantool.Key; +import org.tarantool.TarantoolRequest; +import org.tarantool.schema.TarantoolSchemaMeta; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class UpdateRequestSpec extends SpaceRequestSpec { + + private List key; + private List operations; + + public UpdateRequestSpec(int spaceId, List key, Operation... operations) { + super(Code.UPDATE, spaceId); + this.key = new ArrayList<>(key); + this.operations = Arrays.asList(operations); + } + + public UpdateRequestSpec(String spaceName, List key, Operation... operations) { + super(Code.UPDATE, spaceName); + this.key = new ArrayList<>(key); + this.operations = Arrays.asList(operations); + } + + public UpdateRequestSpec primaryKey(Object... keyParts) { + this.key.clear(); + Collections.addAll(this.key, keyParts); + return this; + } + + public UpdateRequestSpec primaryKey(Collection key) { + this.key.clear(); + this.key.addAll(key); + return this; + } + + public UpdateRequestSpec operations(Operation... operations) { + this.operations.clear(); + Collections.addAll(this.operations, operations); + return this; + } + + public UpdateRequestSpec operations(Collection operations) { + this.operations.clear(); + this.operations.addAll(operations); + return this; + } + + @Override + public TarantoolRequest toTarantoolRequest(TarantoolSchemaMeta schemaMeta) { + TarantoolRequest request = super.toTarantoolRequest(schemaMeta); + request.addArguments( + value(Key.SPACE), + spaceId == null + ? cacheLookupValue(() -> schemaMeta.getSpace(spaceName).getId()) + : value(spaceId), + value(Key.KEY), value(key), + value(Key.TUPLE), value(operations.stream().map(Operation::toArray).collect(Collectors.toList())) + ); + return request; + } + +} diff --git a/src/main/java/org/tarantool/dsl/UpsertRequestSpec.java b/src/main/java/org/tarantool/dsl/UpsertRequestSpec.java new file mode 100644 index 00000000..c1e01023 --- /dev/null +++ b/src/main/java/org/tarantool/dsl/UpsertRequestSpec.java @@ -0,0 +1,88 @@ +package org.tarantool.dsl; + +import static org.tarantool.TarantoolRequestArgumentFactory.cacheLookupValue; +import static org.tarantool.TarantoolRequestArgumentFactory.value; + +import org.tarantool.Code; +import org.tarantool.Key; +import org.tarantool.TarantoolRequest; +import org.tarantool.schema.TarantoolSchemaMeta; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class UpsertRequestSpec extends SpaceRequestSpec { + + private List key; + private List tuple; + private List operations; + + public UpsertRequestSpec(int spaceId, List key, List tuple, Operation... operations) { + super(Code.UPSERT, spaceId); + this.key = new ArrayList<>(key); + this.tuple = new ArrayList<>(tuple); + this.operations = Arrays.asList(operations); + } + + public UpsertRequestSpec(String spaceName, List key, List tuple, Operation... operations) { + super(Code.UPSERT, spaceName); + this.key = new ArrayList<>(key); + this.tuple = new ArrayList<>(tuple); + this.operations = Arrays.asList(operations); + } + + public UpsertRequestSpec primaryKey(Object... keyParts) { + this.key.clear(); + Collections.addAll(this.key, keyParts); + return this; + } + + public UpsertRequestSpec primaryKey(Collection key) { + this.key.clear(); + this.key.addAll(key); + return this; + } + + public UpsertRequestSpec tuple(Collection tuple) { + this.tuple.clear(); + this.tuple.addAll(tuple); + return this; + } + + public UpsertRequestSpec tuple(Object... tupleItems) { + this.tuple.clear(); + Collections.addAll(this.tuple, tupleItems); + return this; + } + + public UpsertRequestSpec operations(Collection operations) { + this.operations.clear(); + this.operations.addAll(operations); + return this; + } + + public UpsertRequestSpec operations(Operation... operations) { + this.operations.clear(); + Collections.addAll(this.operations, operations); + return this; + } + + @Override + public TarantoolRequest toTarantoolRequest(TarantoolSchemaMeta schemaMeta) { + TarantoolRequest request = super.toTarantoolRequest(schemaMeta); + request.addArguments( + value(Key.SPACE), + spaceId == null + ? cacheLookupValue(() -> schemaMeta.getSpace(spaceName).getId()) + : value(spaceId), + value(Key.KEY), value(key), + value(Key.TUPLE), value(tuple), + value(Key.UPSERT_OPS), value(operations.stream().map(Operation::toArray).collect(Collectors.toList())) + ); + return request; + } +} diff --git a/src/test/java/org/tarantool/ClientAsyncOperationsIT.java b/src/test/java/org/tarantool/ClientAsyncOperationsIT.java index 20ead6a3..61d3356f 100644 --- a/src/test/java/org/tarantool/ClientAsyncOperationsIT.java +++ b/src/test/java/org/tarantool/ClientAsyncOperationsIT.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.tarantool.TestAssertions.checkRawTupleResult; +import org.tarantool.dsl.TarantoolRequestSpec; import org.tarantool.schema.TarantoolIndexNotFoundException; import org.tarantool.schema.TarantoolSpaceNotFoundException; @@ -579,6 +580,11 @@ public Future> eval(String expression, Object... args) { return originOps.eval(expression, args).toCompletableFuture(); } + @Override + public Future> execute(TarantoolRequestSpec requestSpec) { + return originOps.execute(requestSpec).toCompletableFuture(); + } + @Override public void ping() { originOps.ping(); From ce50492141ef4a2353e52157182cbf6fe99ee892 Mon Sep 17 00:00:00 2001 From: nicktorwald Date: Fri, 6 Dec 2019 19:19:11 +0700 Subject: [PATCH 2/2] types: ResultSet API for TarantoolClient This commit introduces a new API to handle TarantoolClient result. The concept is similar to the JDBC ResultSet in terms of a getting the data using rows ans columns. Instead of a guessing-style processing the result via List, TarantoolResultSet offers set of typed methods to retrieve the data or get an error if the result cannot be represented as the designated type. Latter case requires to declare formal rules of a casting between the types. In scope of this commit it is supported 11 standard types and conversions between each other. These types are byte, short, int, long, float, double, boolean, BigInteger, BigDecimal, String, and byte[]. Closes: #211 --- README.md | 75 ++ .../java/org/tarantool/InMemoryResultSet.java | 197 ++++ .../java/org/tarantool/TarantoolClient.java | 2 + .../org/tarantool/TarantoolClientImpl.java | 16 + .../org/tarantool/TarantoolResultSet.java | 237 +++++ .../java/org/tarantool/TarantoolTuple.java | 64 ++ .../org/tarantool/conversion/Converter.java | 24 + .../conversion/ConverterRegistry.java | 59 ++ .../conversion/DefaultConverterRegistry.java | 469 +++++++++ .../NotConvertibleValueException.java | 31 + .../tarantool/conversion/StringDecoder.java | 56 + .../java/org/tarantool/util/FloatUtils.java | 132 +++ .../java/org/tarantool/ClientResultSetIT.java | 990 ++++++++++++++++++ .../DefaultConverterRegistryTest.java | 785 ++++++++++++++ 14 files changed, 3137 insertions(+) create mode 100644 src/main/java/org/tarantool/InMemoryResultSet.java create mode 100644 src/main/java/org/tarantool/TarantoolResultSet.java create mode 100644 src/main/java/org/tarantool/TarantoolTuple.java create mode 100644 src/main/java/org/tarantool/conversion/Converter.java create mode 100644 src/main/java/org/tarantool/conversion/ConverterRegistry.java create mode 100644 src/main/java/org/tarantool/conversion/DefaultConverterRegistry.java create mode 100644 src/main/java/org/tarantool/conversion/NotConvertibleValueException.java create mode 100644 src/main/java/org/tarantool/conversion/StringDecoder.java create mode 100644 src/main/java/org/tarantool/util/FloatUtils.java create mode 100644 src/test/java/org/tarantool/ClientResultSetIT.java create mode 100644 src/test/java/org/tarantool/conversion/DefaultConverterRegistryTest.java diff --git a/README.md b/README.md index 659aa410..43a1c2ff 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ To get the Java connector for Tarantool 1.6.9, visit * [Spring NamedParameterJdbcTemplate usage example](#spring-namedparameterjdbctemplate-usage-example) * [JDBC](#JDBC) * [Cluster support](#cluster-support) +* [Getting a result](#getting-a-result) * [Logging](#logging) * [Building](#building) * [Where to get help](#where-to-get-help) @@ -418,6 +419,80 @@ against its integer IDs. 3. The client guarantees an order of synchronous requests per thread. Other cases such as asynchronous or multi-threaded requests may be out of order before the execution. +## Getting a result + +Traditionally, when a response is parsed by the internal MsgPack implementation the client +will return it as a heterogeneous list of objects `List` that in most cases is inconvenient +for users to use. It requires a type guessing as well as a writing more boilerplate code to work +with typed data. Most of the methods which are provided by `TarantoolClientOps` (i.e. `select`) +return raw de-serialized data via `List`. + +Consider a small example how it is usually used: + +```java +// get an untyped array of tuples +List result = client.syncOps().execute(Requests.selectRequest("space", "pk")); +for (int i = 0; i < result.size(); i++) { + // get the first tuple (also untyped) + List row = result.get(i); + // try to cast the first tuple as a couple of values + int id = (int) row.get(0); + String text = (String) row.get(1); + processEntry(id, text); +} +``` + +There is an additional way to work with data using `TarantoolClient.executeRequest(TarantoolRequestConvertible)` +method. This method returns a result wrapper over original data that allows to extract in a more +typed manner rather than it is directly provided by MsgPack serialization. The `executeRequest` +returns the `TarantoolResultSet` which provides a bunch of methods to get data. Inside the result +set the data is represented as a list of rows (tuples) where each row has columns (fields). +In general, it is possible that different rows have different size of their columns in scope of +the same result. + +```java +TarantoolResultSet result = client.executeRequest(Requests.selectRequest("space", "pk")); +while (result.next()) { + long id = result.getLong(0); + String text = result.getString(1); + processEntry(id, text); +} +``` + +The `TarantoolResultSet` provides an implicit conversation between types if it's possible. + +Numeric types internally can represent each other if a type range allows to do it. For example, +byte 100 can be represented as a short, int and other types wider than byte. But 200 integer +cannot be narrowed to a byte because of overflow (byte range is [-128..127]). If a floating +point number is converted to a integer then the fraction part will be omitted. It is also +possible to convert a valid string to a number. + +Boolean type can be obtained from numeric types such as byte, short, int, long, BigInteger, +float and double where 1 (1.0) means true and 0 (0.0) means false. Or it can be got from +a string using well-known patterns such as "1", "t|true", "y|yes", "on" for true and +"0", "f|false", "n|no", "off" for false respectively. + +String type can be converted from a byte array and any numeric types. In case of `byte[]` +all bytes will be interpreted as a UTF-8 sequence. + +There is a special method called `getObject(int, Map)` where a user can provide its own +mapping functions to be applied if a designated type matches a value one. + +For instance, using the following map each strings will be transformed to an upper case and +boolean values will be represented as strings "yes" or "no": + +```java +Map, Function> mappers = new HashMap<>(); +mappers.put( + String.class, + v -> ((String) v).toUpperCase() +); +mappers.put( + Boolean.class, + v -> (boolean) v ? "yes" : "no" +); +``` + ## Spring NamedParameterJdbcTemplate usage example The JDBC driver uses `TarantoolClient` implementation to provide a communication with server. diff --git a/src/main/java/org/tarantool/InMemoryResultSet.java b/src/main/java/org/tarantool/InMemoryResultSet.java new file mode 100644 index 00000000..b8298751 --- /dev/null +++ b/src/main/java/org/tarantool/InMemoryResultSet.java @@ -0,0 +1,197 @@ +package org.tarantool; + +import org.tarantool.conversion.ConverterRegistry; +import org.tarantool.conversion.NotConvertibleValueException; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Simple implementation of {@link TarantoolResultSet} + * that contains all tuples in local memory. + */ +class InMemoryResultSet implements TarantoolResultSet { + + private final ConverterRegistry converterRegistry; + private final List results; + + private int currentIndex; + private List currentTuple; + + InMemoryResultSet(List rawResult, boolean asSingleResult, ConverterRegistry converterRegistry) { + currentIndex = -1; + this.converterRegistry = converterRegistry; + + results = new ArrayList<>(); + ArrayList copiedResult = new ArrayList<>(rawResult); + if (asSingleResult) { + results.add(copiedResult); + } else { + results.addAll(copiedResult); + } + } + + @Override + public boolean next() { + if ((currentIndex + 1) < results.size()) { + currentTuple = getAsTuple(++currentIndex); + return true; + } + return false; + } + + @Override + public boolean previous() { + if ((currentIndex - 1) >= 0) { + currentTuple = getAsTuple(--currentIndex); + return true; + } + return false; + } + + @Override + public byte getByte(int columnIndex) { + return getTypedValue(columnIndex, Byte.class, (byte) 0); + } + + @Override + public short getShort(int columnIndex) { + return getTypedValue(columnIndex, Short.class, (short) 0); + } + + @Override + public int getInt(int columnIndex) { + return getTypedValue(columnIndex, Integer.class, 0); + } + + @Override + public long getLong(int columnIndex) { + return getTypedValue(columnIndex, Long.class, 0L); + } + + @Override + public float getFloat(int columnIndex) { + return getTypedValue(columnIndex, Float.class, 0.0f); + } + + @Override + public double getDouble(int columnIndex) { + return getTypedValue(columnIndex, Double.class, 0.0d); + } + + @Override + public boolean getBoolean(int columnIndex) { + return getTypedValue(columnIndex, Boolean.class, false); + } + + @Override + public byte[] getBytes(int columnIndex) { + return getTypedValue(columnIndex, byte[].class, null); + } + + @Override + public String getString(int columnIndex) { + return getTypedValue(columnIndex, String.class, null); + } + + @Override + public Object getObject(int columnIndex) { + return requireInRange(columnIndex); + } + + @Override + public BigInteger getBigInteger(int columnIndex) { + return getTypedValue(columnIndex, BigInteger.class, null); + } + + @Override + @SuppressWarnings("unchecked") + public List getList(int columnIndex) { + Object value = requireInRange(columnIndex); + if (value == null) { + return null; + } + if (value instanceof List) { + return (List) value; + } + throw new NotConvertibleValueException(value.getClass(), List.class); + } + + @Override + @SuppressWarnings("unchecked") + public Map getMap(int columnIndex) { + Object value = requireInRange(columnIndex);; + if (value == null) { + return null; + } + if (value instanceof Map) { + return (Map) value; + } + throw new NotConvertibleValueException(value.getClass(), Map.class); + } + + @Override + public boolean isNull(int columnIndex) { + Object value = requireInRange(columnIndex); + return value == null; + } + + @Override + public TarantoolTuple getTuple(int size) { + requireInRow(); + int capacity = size == 0 ? currentTuple.size() : size; + return new TarantoolTuple(currentTuple, capacity); + } + + @Override + public int getRowSize() { + return (currentTuple != null) ? currentTuple.size() : -1; + } + + @Override + public boolean isEmpty() { + return results.isEmpty(); + } + + @Override + public void close() { + results.clear(); + currentTuple = null; + currentIndex = -1; + } + + @SuppressWarnings("unchecked") + private R getTypedValue(int columnIndex, Class type, R defaultValue) { + Object value = requireInRange(columnIndex); + if (value == null) { + return defaultValue; + } + if (type.isInstance(value)) { + return (R) value; + } + return converterRegistry.convert(value, type); + } + + @SuppressWarnings("unchecked") + private List getAsTuple(int index) { + Object row = results.get(index); + return (List) row; + } + + private Object requireInRange(int index) { + requireInRow(); + if (index < 1 || index > currentTuple.size()) { + throw new IndexOutOfBoundsException("Index out of range: " + index); + } + return currentTuple.get(index - 1); + } + + private void requireInRow() { + if (currentIndex == -1) { + throw new IllegalArgumentException("Result set out of row position. Try call next() before."); + } + } + +} diff --git a/src/main/java/org/tarantool/TarantoolClient.java b/src/main/java/org/tarantool/TarantoolClient.java index d560d682..65381505 100644 --- a/src/main/java/org/tarantool/TarantoolClient.java +++ b/src/main/java/org/tarantool/TarantoolClient.java @@ -1,5 +1,6 @@ package org.tarantool; +import org.tarantool.dsl.TarantoolRequestSpec; import org.tarantool.schema.TarantoolSchemaMeta; import java.util.List; @@ -33,4 +34,5 @@ public interface TarantoolClient { TarantoolSchemaMeta getSchemaMeta(); + TarantoolResultSet executeRequest(TarantoolRequestSpec requestSpec); } diff --git a/src/main/java/org/tarantool/TarantoolClientImpl.java b/src/main/java/org/tarantool/TarantoolClientImpl.java index ae1d7360..e824f342 100644 --- a/src/main/java/org/tarantool/TarantoolClientImpl.java +++ b/src/main/java/org/tarantool/TarantoolClientImpl.java @@ -1,5 +1,8 @@ package org.tarantool; +import org.tarantool.conversion.ConverterRegistry; +import org.tarantool.conversion.DefaultConverterRegistry; +import org.tarantool.dsl.TarantoolRequestSpec; import org.tarantool.logging.Logger; import org.tarantool.logging.LoggerFactory; import org.tarantool.protocol.ProtoConstants; @@ -92,6 +95,7 @@ public class TarantoolClientImpl extends TarantoolBase> implements Tar protected Thread writer; protected TarantoolSchemaMeta schemaMeta = new TarantoolMetaSpacesCache(this); + protected ConverterRegistry converterRegistry = new DefaultConverterRegistry(); protected Thread connector = new Thread(new Runnable() { @Override @@ -279,6 +283,18 @@ public TarantoolSchemaMeta getSchemaMeta() { return schemaMeta; } + @Override + @SuppressWarnings("unchecked") + public TarantoolResultSet executeRequest(TarantoolRequestSpec requestSpec) { + TarantoolRequest request = requestSpec.toTarantoolRequest(getSchemaMeta()); + List result = (List) syncGet(exec(request)); + return new InMemoryResultSet(result, isSingleResultRow(request.getCode()), converterRegistry); + } + + private boolean isSingleResultRow(Code code) { + return code == Code.EVAL || code == Code.CALL || code == Code.OLD_CALL; + } + /** * Executes an operation with default timeout. * diff --git a/src/main/java/org/tarantool/TarantoolResultSet.java b/src/main/java/org/tarantool/TarantoolResultSet.java new file mode 100644 index 00000000..e06abf3b --- /dev/null +++ b/src/main/java/org/tarantool/TarantoolResultSet.java @@ -0,0 +1,237 @@ +package org.tarantool; + +import java.io.Closeable; +import java.io.InputStream; +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +/** + * A set of data representing a Tarantool result set, which + * is generated by executing a request to the database. + *

+ * A TarantoolResultSet object maintains a cursor pointing + * to its current row of data. Initially the cursor is positioned + * before the first row. The next method moves the + * cursor to the next row and returns false + * when there are no more rows in the result. In the same way the + * previous method works to move backward. + *

+ * A typical use case it to iterate through the result using while loop: + * + *

+ *       TarantoolResultSet result = client.executeRequest(Requests.selectRequest("space", "pk"));
+ *       while (result.next()) {
+ *           long id = result.getLong(0);
+ *           String text = result.getString(1);
+ *           processEntry(id, text);
+ *       }
+ * 
+ * + * The TarantoolResultSet interface provides + * getter methods (getBoolean, getLong, + * and so on) for retrieving column values from the current row using + * column indexes. Columns are numbered from zero. For maximum portability, + * result set columns within each row should be read in left-to-right order, + * and each column should be read only once. + *

+ * For the getter methods, a result set attempts to convert the underlying + * data to the Java type specified in the getter method and returns a suitable + * Java value. + *

+ * There are possible internal conversations: + *

    + *
  1. Between numeric types such as byte, short, int, long, float, double, and big int
  2. + *
  3. Between boolean type and numeric types where zero means false and one means true
  4. + *
  5. Between string type and any other types, if a string represents a convertible value
  6. + *
+ */ +public interface TarantoolResultSet extends Closeable { + + /** + * Describes a variable size that equals current tuple size. + */ + int ACTUAL_ROW_SIZE = 0; + + /** + * Shift a result by one tuple next. + * Must be invoked + * + * @return {@literal true} if next tuple is selected + */ + boolean next(); + + /** + * Moves a result to on tuple before, if it is possible. + * + * @return {@literal true} if previous tuple is selected + */ + boolean previous(); + + /** + * Gets a tuple value as a byte. + * + * @param columnIndex 1-based index + * + * @return value as a byte or zero if value is null + */ + byte getByte(int columnIndex); + + /** + * Gets a tuple value as a short. + * + * @param columnIndex 1-based index + * + * @return value as a short or zero if value is null + */ + short getShort(int columnIndex); + + /** + * Gets a tuple value as an integer. + * + * @param columnIndex 1-based index + * + * @return value as an integer or zero if value is null + */ + int getInt(int columnIndex); + + /** + * Gets a tuple value as a long integer. + * + * @param columnIndex 1-based index + * + * @return value as a long or zero if value is null + */ + long getLong(int columnIndex); + + /** + * Gets a tuple value as a {@link java.math.BigInteger}. + * + * @param columnIndex 1-based index + * + * @return value as a long or zero if value is null + */ + BigInteger getBigInteger(int columnIndex); + + /** + * Gets a tuple value as a boolean. + * + * @param columnIndex 1-based index + * + * @return value as a boolean or {@literal false} if value is null + */ + boolean getBoolean(int columnIndex); + + /** + * Gets a tuple value as a float. + * + * @param columnIndex 1-based index + * + * @return value as a float or zero if value is null + */ + float getFloat(int columnIndex); + + /** + * Gets a tuple value as a double. + * + * @param columnIndex 1-based index + * + * @return value as a double or zero if value is null + */ + double getDouble(int columnIndex); + + /** + * Gets a tuple value as a byte array. + * + * @param columnIndex 1-based index + * + * @return value as a byte array or {@literal null} + */ + byte[] getBytes(int columnIndex); + + /** + * Gets a tuple value as a string. + * + * @param columnIndex 1-based index + * + * @return value as a string or {@literal null} + */ + String getString(int columnIndex); + + /** + * Gets a tuple value as is. + * The actual value type is a type serialized by MsgPack. + * This method is similar to {@code getObject(columnIndex, Collections.emptyMap())}. + * + * @param columnIndex 1-based index + * + * @return value as a string or {@literal null} + * + * @see MsgPackLite#unpack(InputStream) + */ + Object getObject(int columnIndex); + + /** + * Gets a tuple value as a list. + * Typical list value is a Tarantool array such + * as {@code {'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'}}. + * + * @param columnIndex 1-based index + * + * @return value as a list or {@literal null} + */ + List getList(int columnIndex); + + /** + * Gets a tuple value as a map. + * Typical map value is a Tarantool map such as {@code {x=0,y=1,color='red'}}. + * + * @param columnIndex 1-based index + * + * @return value as a map or {@literal null} + */ + Map getMap(int columnIndex); + + /** + * Checks whether a designated column is null. + * This method can be invoked in addition to primitive get{Byte,Short,Int,Long} + * methods to distinguish the zero results. + * + * @param columnIndex 1-based index + * + * @return {@literal true} if column is null + */ + boolean isNull(int columnIndex); + + /** + * Gets a current row as a tuple of the specified length. + * If size is bigger than actual row size all remaining elements + * will be filled as {@code null}. + * If {@link #ACTUAL_ROW_SIZE} is passed then a tuple will be + * return containing exactly same size as the original row. + * + *

+ * {@code result.getRow(ACTUAL_ROW_SIZE)} logically equals + * {@code result.getRow(result.getRowSize()}. + * + * @return tuple which has the required size + * + * @throws IllegalArgumentException if {@code size} is out of [0..tupleSize] range. + */ + TarantoolTuple getTuple(int size); + + /** + * Get a number of columns for the current tuple. + * + * @return tuple size + */ + int getRowSize(); + + /** + * Checks whether a result is empty. + * + * @return {@literal true} if no tuples are received + */ + boolean isEmpty(); +} + diff --git a/src/main/java/org/tarantool/TarantoolTuple.java b/src/main/java/org/tarantool/TarantoolTuple.java new file mode 100644 index 00000000..5175094d --- /dev/null +++ b/src/main/java/org/tarantool/TarantoolTuple.java @@ -0,0 +1,64 @@ +package org.tarantool; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tarantool row wrapper. Can be used to obtain elements via + * 1-based index. + * + *

+ * The theoretical tuple size is 2^31-1 elements. Thus, it leads + * a client can request a too large tuple filled by nulls. To handle + * this case in a less expensive way, this container provides a getting + * such nulls without an extra memory allocation till the user request + * via {@link #toList()} or {@link #toArray()}. + * + *

+ * XXX: consider a implementation of {@link java.util.List} instead + * (see examples in {@link java.util.Collections}). This will allow + * to work with the tuple through {@code List} interface directly. + */ +public class TarantoolTuple { + private final List source; + private final int capacity; + + public TarantoolTuple(List source, int capacity) { + if (source.size() > capacity) { + throw new IllegalArgumentException("Tuple capacity is less than source list"); + } + this.source = source; + this.capacity = capacity; + } + + public Object get(int index) { + if (index < 1 || index > capacity) { + throw new IndexOutOfBoundsException("Index out of range: " + index); + } + if (index > source.size()) { + return null; + } + return source.get(index - 1); + } + + public int size() { + return capacity; + } + + public List toList() { + ArrayList list = new ArrayList<>(capacity); + list.addAll(source); + for (int i = list.size(); i < capacity; i++) { + list.add(null); + } + return list; + } + + public Object[] toArray() { + Object[] array = new Object[capacity]; + for (int i = 0; i < source.size(); i++) { + array[i] = source.get(i); + } + return array; + } +} diff --git a/src/main/java/org/tarantool/conversion/Converter.java b/src/main/java/org/tarantool/conversion/Converter.java new file mode 100644 index 00000000..45dcb224 --- /dev/null +++ b/src/main/java/org/tarantool/conversion/Converter.java @@ -0,0 +1,24 @@ +package org.tarantool.conversion; + +/** + * Conversion unit used to transform + * values from one type to another. + * + * @param source type + * @param target type + */ +public interface Converter { + + /** + * Converts a value of a source type to + * target. + * + * @param source value of the source type + * + * @return converted value of target type + * + * @throws NotConvertibleValueException if error occurs while conversation + */ + B convert(A source); + +} diff --git a/src/main/java/org/tarantool/conversion/ConverterRegistry.java b/src/main/java/org/tarantool/conversion/ConverterRegistry.java new file mode 100644 index 00000000..12a8ca3d --- /dev/null +++ b/src/main/java/org/tarantool/conversion/ConverterRegistry.java @@ -0,0 +1,59 @@ +package org.tarantool.conversion; + +/** + * Entry point for internal type conversions. + * It uses set of registered converters to perform + * transformations between types. + */ +public interface ConverterRegistry { + + /** + * Checks whether an available converter exists between + * specified types or not. + * + * @param from source type + * @param to target type + * + * @return {@literal true} if two types are convertible + */ + boolean isConvertible(Class from, Class to); + + /** + * Converts the source value to target type value using + * an appropriate registered converter. + * + * @param object source object + * @param targetType destination type class + * @param target type + * + * @return converted value + * + * @throws NotConvertibleValueException if conversion is not supported or failed + */ + B convert(Object object, Class targetType); + + /** + * Registers a new converter between two types. + * It replaces the previous one converter if latter + * was registered before. + * + * @param from source type class + * @param to target type class + * @param converter converter from source to target types + * @param source type + * @param target type + */ + void addConverter(Class from, Class to, Converter converter); + + /** + * Unregisters previously added converter from source + * to target types. + * + * @param from source type + * @param to target type + * + * @return {@literal true} if a converter was found and unregistered + */ + boolean removeConvertible(Class from, Class to); + +} diff --git a/src/main/java/org/tarantool/conversion/DefaultConverterRegistry.java b/src/main/java/org/tarantool/conversion/DefaultConverterRegistry.java new file mode 100644 index 00000000..66119168 --- /dev/null +++ b/src/main/java/org/tarantool/conversion/DefaultConverterRegistry.java @@ -0,0 +1,469 @@ +package org.tarantool.conversion; + +import org.tarantool.util.FloatUtils; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Registers default converters between types than can be + * implicitly converted using, for example, in {@link org.tarantool.TarantoolResultSet}. + * + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Built-in supported convertible pairs
↓ from \ to →byte short intlong float doubleboolean BigInteger BigDecimalString byte[]
bytex + ++ + +-/+ + ++ -
short-/+ x ++ + +-/+ + ++ -
int-/+ -/+ x+ -/+ +-/+ + ++ -
long-/+ -/+ -/+x -/+ -/+-/+ + ++ -
float-/+ -/+ -/+-/+ x +/--/+ -/+ -/++/- -
double-/+ -/+ -/+-/+ +/- x-/+ -/+ -/++/- -
boolean- - -- - -x - -+ -
BigInteger-/+ -/+ -/+-/+ -/+ -/+-/+ x ++ -
BigDecimal-/+ -/+ -/+-/+ +/- +/--/+ -/+ x+/- -
String-/+ -/+ -/+-/+ -/+ -/+-/+ -/+ -/+x +
byte[]- - -- - -- - --/+ x
+ * + *

+ * Where: + *

    + *
  • x means not applicable conversion
  • + *
  • - means not supported conversion
  • + *
  • + means supported without loss of info
  • + *
  • + * +/- means supported with possible loss of precision + * that will be silently ignored + *
  • + *
  • + * -/+ means supported with possible loss of precision but + * a conversion may raise an exception in the case + *
  • + *
+ * + *

+ * There are a few rules applied while conversions: + * + *

    + *
  • + * Conversions between float, double, and BigDecimal types are similar + * to the narrowing/widening primitive conversion as defined in + * The JLS (5.1.2, 5.1.3). + *
  • + *
  • + * Conversions from float, double, BigDecimal to String + * may produce inexact result that is depended on {@code toString()} + * implementation of the specified classes. + *
  • + *
  • + * Conversion between numbers and {@code boolean} is concluded in + * comparisons like {@code number == {0|1}}; ({@code true} if a number is + * one and {@code false} if a number is zero). + *
  • + *
  • + * Conversion from string to boolean follows the rule where + * {@code "true"} and {@code "false"} strings are converted to {@code true} + * and {@code false} boolean values respectively. Any other strings are not + * supported. + *
  • + *
  • + * Conversion between {@code String} and {@code byte[]} and vice-versa + * is performed using {@literal UTF-8} encoding charset. + *
  • + *
+ * + * @see org.tarantool.TarantoolResultSet + */ +public class DefaultConverterRegistry implements ConverterRegistry { + + private final Map> converters = new HashMap<>(); + + public DefaultConverterRegistry() { + registerToByteConverters(); + registerToShortConverters(); + registerToIntConverters(); + registerToLongConverters(); + + registerToFloatConverters(); + registerToDoubleConverters(); + + registerToBooleanConverters(); + + registerToBigIntegerConverters(); + registerToBigDecimalConverters(); + + registerToStringConverters(); + registerToByteArrayConverters(); + } + + private void registerToByteConverters() { + Converter defaultNumberToByteConverter = + number -> this.numberToSafeLong(number, Byte.MIN_VALUE, Byte.MAX_VALUE, "Byte").byteValue(); + + addConverter(Short.class, Byte.class, defaultNumberToByteConverter); + addConverter(Integer.class, Byte.class, defaultNumberToByteConverter); + addConverter(Long.class, Byte.class, defaultNumberToByteConverter); + addConverter(Float.class, Byte.class, defaultNumberToByteConverter); + addConverter(Double.class, Byte.class, defaultNumberToByteConverter); + addConverter(BigInteger.class, Byte.class, defaultNumberToByteConverter); + addConverter(BigDecimal.class, Byte.class, defaultNumberToByteConverter); + addConverter(String.class, Byte.class, Byte::parseByte); + } + + private void registerToShortConverters() { + Converter defaultNumberToShortConverter = number -> + this.numberToSafeLong(number, Short.MIN_VALUE, Short.MAX_VALUE, "Short").shortValue(); + + addConverter(Byte.class, Short.class, defaultNumberToShortConverter); + addConverter(Integer.class, Short.class, defaultNumberToShortConverter); + addConverter(Long.class, Short.class, defaultNumberToShortConverter); + addConverter(Float.class, Short.class, defaultNumberToShortConverter); + addConverter(Double.class, Short.class, defaultNumberToShortConverter); + addConverter(BigInteger.class, Short.class, defaultNumberToShortConverter); + addConverter(BigDecimal.class, Short.class, defaultNumberToShortConverter); + addConverter(String.class, Short.class, Short::parseShort); + } + + private void registerToIntConverters() { + Converter defaultNumberToIntegerConverter = + number -> this.numberToSafeLong(number, Integer.MIN_VALUE, Integer.MAX_VALUE, "Integer").intValue(); + + addConverter(Byte.class, Integer.class, defaultNumberToIntegerConverter); + addConverter(Short.class, Integer.class, defaultNumberToIntegerConverter); + addConverter(Long.class, Integer.class, defaultNumberToIntegerConverter); + addConverter(Float.class, Integer.class, defaultNumberToIntegerConverter); + addConverter(Double.class, Integer.class, defaultNumberToIntegerConverter); + addConverter(BigInteger.class, Integer.class, defaultNumberToIntegerConverter); + addConverter(BigDecimal.class, Integer.class, defaultNumberToIntegerConverter); + addConverter(String.class, Integer.class, Integer::parseInt); + } + + private void registerToLongConverters() { + Converter defaultNumberToLongConverter = + number -> this.numberToSafeLong(number, Long.MIN_VALUE, Long.MAX_VALUE, "Long"); + addConverter(Byte.class, Long.class, defaultNumberToLongConverter); + addConverter(Short.class, Long.class, defaultNumberToLongConverter); + addConverter(Integer.class, Long.class, defaultNumberToLongConverter); + addConverter(Float.class, Long.class, defaultNumberToLongConverter); + addConverter(Double.class, Long.class, defaultNumberToLongConverter); + addConverter(BigInteger.class, Long.class, defaultNumberToLongConverter); + addConverter(BigDecimal.class, Long.class, defaultNumberToLongConverter); + addConverter(String.class, Long.class, Long::parseLong); + } + + private void registerToFloatConverters() { + Converter defaultNumberToFloatConverter = Number::floatValue; + + addConverter(Byte.class, Float.class, defaultNumberToFloatConverter); + addConverter(Short.class, Float.class, defaultNumberToFloatConverter); + addConverter(Integer.class, Float.class, this::numberToFloat); + addConverter(Long.class, Float.class, this::numberToFloat); + addConverter(Double.class, Float.class, defaultNumberToFloatConverter); + addConverter(BigInteger.class, Float.class, this::numberToFloat); + addConverter(BigDecimal.class, Float.class, defaultNumberToFloatConverter); + addConverter(String.class, Float.class, Float::parseFloat); + } + + private void registerToDoubleConverters() { + Converter defaultNumberToDoubleConverter = Number::doubleValue; + + addConverter(Byte.class, Double.class, defaultNumberToDoubleConverter); + addConverter(Short.class, Double.class, defaultNumberToDoubleConverter); + addConverter(Integer.class, Double.class, defaultNumberToDoubleConverter); + addConverter(Long.class, Double.class, this::numberToDouble); + addConverter(Float.class, Double.class, defaultNumberToDoubleConverter); + addConverter(BigInteger.class, Double.class, this::numberToDouble); + addConverter(BigDecimal.class, Double.class, defaultNumberToDoubleConverter); + addConverter(String.class, Double.class, Double::parseDouble); + } + + private void registerToBooleanConverters() { + Converter defaultNumberToBooleanConverter = + number -> this.numberToSafeLong(number, 0, 1, "Boolean").equals(1L); + + addConverter(Byte.class, Boolean.class, defaultNumberToBooleanConverter); + addConverter(Short.class, Boolean.class, defaultNumberToBooleanConverter); + addConverter(Integer.class, Boolean.class, defaultNumberToBooleanConverter); + addConverter(Long.class, Boolean.class, defaultNumberToBooleanConverter); + addConverter(Float.class, Boolean.class, defaultNumberToBooleanConverter); + addConverter(Double.class, Boolean.class, defaultNumberToBooleanConverter); + addConverter(BigInteger.class, Boolean.class, defaultNumberToBooleanConverter); + addConverter(BigDecimal.class, Boolean.class, defaultNumberToBooleanConverter); + addConverter(String.class, Boolean.class, string -> { + if ("0".equalsIgnoreCase(string) || "false".equalsIgnoreCase(string)) { + return false; + } + if ("1".equalsIgnoreCase(string) || "true".equalsIgnoreCase(string)) { + return true; + } + throw new NotConvertibleValueException(String.class, Boolean.class); + }); + } + + private void registerToBigIntegerConverters() { + Converter defaultLongToIntegerConverter = number -> BigInteger.valueOf(number.longValue()); + + addConverter(Byte.class, BigInteger.class, defaultLongToIntegerConverter); + addConverter(Short.class, BigInteger.class, defaultLongToIntegerConverter); + addConverter(Integer.class, BigInteger.class, defaultLongToIntegerConverter); + addConverter(Long.class, BigInteger.class, defaultLongToIntegerConverter); + addConverter(Float.class, BigInteger.class, number -> new BigDecimal(number).toBigIntegerExact()); + addConverter(Double.class, BigInteger.class, number -> new BigDecimal(number).toBigIntegerExact()); + addConverter(BigDecimal.class, BigInteger.class, BigDecimal::toBigIntegerExact); + addConverter(String.class, BigInteger.class, string -> new BigDecimal(string).toBigIntegerExact()); + } + + private void registerToBigDecimalConverters() { + Converter defaultLongToDecimalConverter = number -> BigDecimal.valueOf(number.longValue()); + addConverter(Byte.class, BigDecimal.class, defaultLongToDecimalConverter); + addConverter(Short.class, BigDecimal.class, defaultLongToDecimalConverter); + addConverter(Integer.class, BigDecimal.class, defaultLongToDecimalConverter); + addConverter(Long.class, BigDecimal.class, defaultLongToDecimalConverter); + + Converter defaultDoubleToDecimalConverter = number -> new BigDecimal(number.doubleValue()); + addConverter(Float.class, BigDecimal.class, defaultDoubleToDecimalConverter); + addConverter(Double.class, BigDecimal.class, defaultDoubleToDecimalConverter); + + addConverter(BigInteger.class, BigDecimal.class, BigDecimal::new); + addConverter(String.class, BigDecimal.class, BigDecimal::new); + } + + private void registerToStringConverters() { + Converter defaultToStringConverter = Object::toString; + + addConverter(Byte.class, String.class, defaultToStringConverter); + addConverter(Short.class, String.class, defaultToStringConverter); + addConverter(Integer.class, String.class, defaultToStringConverter); + addConverter(Long.class, String.class, defaultToStringConverter); + addConverter(Float.class, String.class, defaultToStringConverter); + addConverter(Double.class, String.class, defaultToStringConverter); + addConverter(BigInteger.class, String.class, defaultToStringConverter); + addConverter(BigDecimal.class, String.class, defaultToStringConverter); + addConverter(Boolean.class, String.class, defaultToStringConverter); + addConverter(byte[].class, String.class, bytes -> { + try { + // strict UTF-8 decoding + return StringDecoder.getDecoder(StandardCharsets.UTF_8).decode(ByteBuffer.wrap(bytes)).toString(); + } catch (CharacterCodingException cce) { + throw new NotConvertibleValueException(byte[].class, String.class, cce); + } + }); + } + + private void registerToByteArrayConverters() { + addConverter(String.class, byte[].class, string -> string.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public boolean isConvertible(Class from, Class to) { + return converters.containsKey(new ConvertiblePair(from, to)); + } + + @Override + @SuppressWarnings("unchecked") + public B convert(Object object, Class targetType) { + Converter converter = + (Converter) converters.get(new ConvertiblePair(object.getClass(), targetType)); + if (converter == null) { + throw new NotConvertibleValueException(object.getClass(), targetType); + } + try { + return converter.convert(object); + } catch (Exception cause) { + if (cause instanceof NotConvertibleValueException) { + throw cause; + } + throw new NotConvertibleValueException(object.getClass(), targetType, cause); + } + } + + @Override + public void addConverter(Class
from, Class to, Converter converter) { + converters.put(new ConvertiblePair(from, to), converter); + } + + @Override + public boolean removeConvertible(Class from, Class to) { + return converters.remove(new ConvertiblePair(from, to)) != null; + } + + /** + * Performs type conversion from {Code Number} to {@code Long} types and + * checks possible overflow issues. + * + * @param value number to be converted to Long + * @param min lower target type bound + * @param max upper target type bound + * @param type target type name + * + * @return converted value as {@code long} + */ + private Long numberToSafeLong(Number value, long min, long max, String type) { + long number = 0L; + try { + if (value instanceof BigInteger) { + number = ((BigInteger) value).longValueExact(); + } else if (value instanceof BigDecimal) { + number = ((BigDecimal) value).longValueExact(); + } else if (value instanceof Float || value instanceof Double) { + if (!FloatUtils.isLongExact(value.doubleValue())) { + throw new ArithmeticException("Not a long value"); + } + number = value.longValue(); + } else { + number = value.longValue(); + } + } catch (ArithmeticException ignored) { + throwOutOfRangeException(value, min, max, type); + } + if (number < min || number > max) { + throwOutOfRangeException(number, min, max, type); + } + return number; + } + + private Float numberToFloat(Number value) { + if (FloatUtils.isFloatExact(value.longValue())) { + return value.floatValue(); + } + throw new RuntimeException("Not an integral float"); + } + + private Float numberToFloat(BigInteger value) { + if (FloatUtils.isBigFloatExact(value)) { + return value.floatValue(); + } + throw new RuntimeException("Not an integral float"); + } + + private Double numberToDouble(Number value) { + if (FloatUtils.isDoubleExact(value.longValue())) { + return value.doubleValue(); + } + throw new RuntimeException("Not an integral double"); + } + + private Double numberToDouble(BigInteger value) { + if (FloatUtils.isBigDoubleExact(value)) { + return value.doubleValue(); + } + throw new RuntimeException("Not an integral double"); + } + + private void throwOutOfRangeException(Number value, long min, long max, String type) { + throw new RuntimeException( + value + " is out of " + type + " range [" + min + ".." + max + "]" + ); + } + + private static class ConvertiblePair { + private final Class fromType; + private final Class toType; + + public ConvertiblePair(Class fromType, Class toType) { + this.fromType = fromType; + this.toType = toType; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + ConvertiblePair that = (ConvertiblePair) other; + return Objects.equals(fromType, that.fromType) && + Objects.equals(toType, that.toType); + } + + @Override + public int hashCode() { + return Objects.hash(fromType, toType); + } + } +} diff --git a/src/main/java/org/tarantool/conversion/NotConvertibleValueException.java b/src/main/java/org/tarantool/conversion/NotConvertibleValueException.java new file mode 100644 index 00000000..047677f4 --- /dev/null +++ b/src/main/java/org/tarantool/conversion/NotConvertibleValueException.java @@ -0,0 +1,31 @@ +package org.tarantool.conversion; + +/** + * Raised if an attempt of conversion is failed. + */ +public class NotConvertibleValueException extends RuntimeException { + + private final Class from; + private final Class to; + + public NotConvertibleValueException(Class from, Class to) { + super("Could not convert types " + from + " -> " + to + ". Unsupported conversion."); + this.from = from; + this.to = to; + } + + public NotConvertibleValueException(Class from, Class to, Throwable cause) { + super("Could not convert types " + from + " -> " + to, cause); + this.from = from; + this.to = to; + } + + public Class getFrom() { + return from; + } + + public Class getTo() { + return to; + } + +} diff --git a/src/main/java/org/tarantool/conversion/StringDecoder.java b/src/main/java/org/tarantool/conversion/StringDecoder.java new file mode 100644 index 00000000..5759b097 --- /dev/null +++ b/src/main/java/org/tarantool/conversion/StringDecoder.java @@ -0,0 +1,56 @@ +package org.tarantool.conversion; + +import java.lang.ref.SoftReference; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.util.Objects; + +/** + * String decoders factory. + * + *

+ * The decoders are heavyweight objects to be created each time they are + * requested. On the other hand, the decoders are not thread-safe and cannot + * be reused at the same time. + * This cache uses {@link java.lang.ThreadLocal} to keep a local copies of + * the decoder per each tread. + */ +class StringDecoder { + + private static final ThreadLocal> decoderLocal = new ThreadLocal<>(); + + /** + * Gets a decoder of the specified charset. + * + * @param charset target charset + * + * @return decoder + */ + public static CharsetDecoder getDecoder(Charset charset) { + Objects.requireNonNull(charset); + CharsetDecoder decoder = unwrap(decoderLocal); + if (decoder == null) { + decoder = charset.newDecoder(); + wrap(decoderLocal, decoder); + return decoder; + } + if (!decoder.charset().equals(charset)) { + decoder = charset.newDecoder(); + wrap(decoderLocal, decoder); + } + return decoder; + } + + private static void wrap(ThreadLocal> local, T object) { + local.set(new SoftReference<>(object)); + } + + private static T unwrap(ThreadLocal> local) { + SoftReference softReference = local.get(); + if (softReference == null) { + return null; + } + return softReference.get(); + } + +} diff --git a/src/main/java/org/tarantool/util/FloatUtils.java b/src/main/java/org/tarantool/util/FloatUtils.java new file mode 100644 index 00000000..de0f7509 --- /dev/null +++ b/src/main/java/org/tarantool/util/FloatUtils.java @@ -0,0 +1,132 @@ +package org.tarantool.util; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * Set of convenient methods to work with floats and doubles. + */ +public class FloatUtils { + + /** + * Max float constant as an integer (2^127 * (2 − 2^-23)). + */ + public static final BigInteger MAX_BIG_FLOAT = new BigDecimal(Float.MAX_VALUE).toBigInteger(); + + /** + * Minimal long value as a exact double. + */ + public static double MIN_LONG_DOUBLE = (double) Long.MIN_VALUE; + + /** + * Max double constant as an integer (2^1023 * (2 − 2^-52)). + */ + public static BigInteger MAX_BIG_DOUBLE = new BigDecimal(Double.MAX_VALUE).toBigInteger(); + + /** + * Missed public double constants from {@link Double}. + */ + public static final int DOUBLE_SIGNIFICAND_WIDTH = 52; + public static final long DOUBLE_SIGNIFICAND_MASK = 0x000FFFFFFFFFFFFFL; + public static final long DOUBLE_SIGNIFICAND_HIDDEN_BIT = 0x0010000000000000L; + + /** + * Missed public double constants from {@link Float}. + */ + public static final int FLOAT_SIGNIFICAND_WIDTH = 23; + + /** + * Checks whether a long is precisely representable as a float. + * + * @param value big integer value to be checked + * + * @return {@literal true} if the given long can be exactly stored as a float + */ + public static boolean isFloatExact(long value) { + value = Math.abs(value); + int insignificantBitsCount = Long.numberOfTrailingZeros(value) + Long.numberOfLeadingZeros(value); + // +32 because of a long extension part + return insignificantBitsCount >= (31 - FLOAT_SIGNIFICAND_WIDTH + 32); + } + + /** + * Checks whether a long is precisely representable as a double. + * + * @param value big integer value to be checked + * + * @return {@literal true} if the given long can be exactly stored as a double + */ + public static boolean isDoubleExact(long value) { + value = Math.abs(value); + int insignificantBitsCount = Long.numberOfTrailingZeros(value) + Long.numberOfLeadingZeros(value); + return insignificantBitsCount >= (63 - DOUBLE_SIGNIFICAND_WIDTH); + } + + /** + * Checks whether a big integer is precisely representable as a float. + * + * @param value big integer value to be checked + * + * @return {@literal true} if the given big integer can be exactly stored as a float + */ + public static boolean isBigFloatExact(BigInteger value) { + value = value.abs(); + if (value.abs().compareTo(MAX_BIG_FLOAT) > 0) { + return false; + } + return (value.bitLength() - value.getLowestSetBit()) <= (FLOAT_SIGNIFICAND_WIDTH + 1); + } + + /** + * Checks whether a big integer is precisely representable as a double. + * + * @param value big integer value to be checked + * + * @return {@literal true} if the given big integer can be exactly stored as a double + */ + public static boolean isBigDoubleExact(BigInteger value) { + value = value.abs(); + if (value.compareTo(MAX_BIG_DOUBLE) > 0) { + return false; + } + return (value.bitLength() - value.getLowestSetBit()) <= (DOUBLE_SIGNIFICAND_WIDTH + 1); + } + + /** + * Checks whether a double can be casted to {@literal long} without + * loss of data. + * + *

+ * It is mostly inspired by Google Guava {@code DoubleMath.isMathematicalInteger} + * but it does not take into account integral values being out of {@literal long} + * range. + * + * @param value target value to be checked + * + * @return {@literal true} if the given double represents an exact long value + */ + public static boolean isLongExact(double value) { + if (value == MIN_LONG_DOUBLE || value == 0.0) { + return true; + } + int exp = Math.getExponent(value); + return exp < 63 && DOUBLE_SIGNIFICAND_WIDTH - Long.numberOfTrailingZeros(getSignificand(value)) <= exp; + } + + /** + * Extracts double significand according to IEEE 754. + * It's also includes an applying of an implicit bit. + * + * @param value finite double value + * + * @return double significand as a long value + * + * @see Double#longBitsToDouble(long) + */ + private static long getSignificand(double value) { + int exponent = Math.getExponent(value); + long doubleBits = Double.doubleToRawLongBits(value) & DOUBLE_SIGNIFICAND_MASK; + return (exponent == Double.MIN_EXPONENT - 1) ? (doubleBits << 1) : (doubleBits | DOUBLE_SIGNIFICAND_HIDDEN_BIT); + } + +} diff --git a/src/test/java/org/tarantool/ClientResultSetIT.java b/src/test/java/org/tarantool/ClientResultSetIT.java new file mode 100644 index 00000000..e1c59b25 --- /dev/null +++ b/src/test/java/org/tarantool/ClientResultSetIT.java @@ -0,0 +1,990 @@ +package org.tarantool; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.tarantool.conversion.NotConvertibleValueException; +import org.tarantool.dsl.Requests; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; + +public class ClientResultSetIT { + + private static TarantoolTestHelper testHelper; + + private TarantoolClient client; + + @BeforeAll + static void setupEnv() { + testHelper = new TarantoolTestHelper("client-resultset-it"); + testHelper.createInstance(); + testHelper.startInstance(); + } + + @BeforeEach + void setUp() { + client = TestUtils.makeTestClient(TestUtils.makeDefaultClientConfig(), 2000); + } + + @AfterEach + void tearDown() { + client.close(); + } + + @AfterAll + static void tearDownEnv() { + testHelper.stopInstance(); + } + + @Test + void testGetSimpleRows() { + testHelper.executeLua( + "box.schema.space.create('basic_test', { format = " + + "{{name = 'id', type = 'integer'}," + + "{name = 'num', type = 'integer', is_nullable = true}," + + "{name = 'val', type = 'string', is_nullable = true} }})", + "box.space.basic_test:create_index('pk', { type = 'TREE', parts = {'id'} } )", + "box.space.basic_test:insert{1, nil, 'string'}", + "box.space.basic_test:insert{2, 50, nil}", + "box.space.basic_test:insert{3, 123, 'some'}", + "box.space.basic_test:insert{4, -89, '89'}", + "box.space.basic_test:insert{5, 93127, 'too many'}", + "box.space.basic_test:insert{6, nil, nil}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("basic_test", "pk")); + resultSet.next(); + assertEquals(0, resultSet.getInt(2)); + assertEquals("string", resultSet.getString(3)); + + // all ending nil values are trimmed + resultSet.next(); + assertEquals(50, resultSet.getInt(2)); + assertEquals(2, resultSet.getRowSize()); + + resultSet.next(); + assertEquals(123, resultSet.getInt(2)); + assertEquals("some", resultSet.getString(3)); + + resultSet.next(); + assertEquals(-89, resultSet.getInt(2)); + assertEquals("89", resultSet.getString(3)); + + resultSet.next(); + assertEquals(93127, resultSet.getInt(2)); + assertEquals("too many", resultSet.getString(3)); + + // all ending nil values are trimmed + resultSet.next(); + assertEquals(1, resultSet.getRowSize()); + + dropSpace("basic_test"); + } + + @Test + void testResultTraversal() { + testHelper.executeLua( + "box.schema.space.create('basic_test', {format={{name = 'id', type = 'integer'}}})", + "box.space.basic_test:create_index('pk', { type = 'TREE', parts = {'id'} } )", + "box.space.basic_test:insert{1}", + "box.space.basic_test:insert{2}", + "box.space.basic_test:insert{3}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("basic_test", "pk")); + + assertThrows(IllegalArgumentException.class, () -> resultSet.getInt(1)); + + assertTrue(resultSet.next()); + assertEquals(1, resultSet.getInt(1)); + + assertTrue(resultSet.next()); + assertEquals(2, resultSet.getInt(1)); + + assertTrue(resultSet.next()); + assertEquals(3, resultSet.getInt(1)); + + assertFalse(resultSet.next()); + + assertTrue(resultSet.previous()); + assertEquals(2, resultSet.getInt(1)); + + assertTrue(resultSet.previous()); + assertEquals(1, resultSet.getInt(1)); + + assertFalse(resultSet.previous()); + + dropSpace("basic_test"); + } + + @Test + void testResultClose() throws IOException { + testHelper.executeLua( + "box.schema.space.create('basic_test', {format={{name = 'id', type = 'integer'}}})", + "box.space.basic_test:create_index('pk', { type = 'TREE', parts = {'id'} } )", + "box.space.basic_test:insert{1}", + "box.space.basic_test:insert{2}", + "box.space.basic_test:insert{3}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("basic_test", "pk")); + + resultSet.next(); + assertEquals(1, resultSet.getRowSize()); + + resultSet.close(); + assertEquals(-1, resultSet.getRowSize()); + + dropSpace("basic_test"); + } + + @Test + void testGetEmptyResult() { + testHelper.executeLua( + "box.schema.space.create('basic_test', {format={{name = 'id', type = 'integer'}}})", + "box.space.basic_test:create_index('pk', { type = 'TREE', parts = {'id'} } )" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("basic_test", "pk")); + + assertTrue(resultSet.isEmpty()); + assertFalse(resultSet.next()); + + dropSpace("basic_test"); + } + + @Test + void testGetWrongColumn() { + testHelper.executeLua( + "box.schema.space.create('basic_test', {format={{name = 'id', type = 'integer'}}})", + "box.space.basic_test:create_index('pk', { type = 'TREE', parts = {'id'} } )", + "box.space.basic_test:insert{1}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("basic_test", "pk")); + + resultSet.next(); + assertThrows(IndexOutOfBoundsException.class, () -> resultSet.getByte(2)); + + dropSpace("basic_test"); + } + + @Test + void testGetByteValue() { + testHelper.executeLua( + "box.schema.space.create('byte_vals', { format = " + + "{{name = 'id', type = 'integer'}, {name = 'byte_val', type = 'integer', is_nullable = true} }})", + "box.space.byte_vals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.byte_vals:insert{1, -128}", + "box.space.byte_vals:insert{2, 127}", + "box.space.byte_vals:insert{3, nil}", + "box.space.byte_vals:insert{4, 0}", + "box.space.byte_vals:insert{5, 15}", + "box.space.byte_vals:insert{6, 114}", + "box.space.byte_vals:insert{7, -89}", + "box.space.byte_vals:insert{8, 300}", + "box.space.byte_vals:insert{9, -250}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("byte_vals", "pk")); + + resultSet.next(); + assertEquals(-128, resultSet.getByte(2)); + + resultSet.next(); + assertEquals(127, resultSet.getByte(2)); + + // last nil value is trimmed + resultSet.next(); + assertEquals(1, resultSet.getRowSize()); + + resultSet.next(); + assertEquals(0, resultSet.getByte(2)); + assertFalse(resultSet.isNull(2)); + + resultSet.next(); + assertEquals(15, resultSet.getByte(2)); + + resultSet.next(); + assertEquals(114, resultSet.getByte(2)); + + resultSet.next(); + assertEquals(-89, resultSet.getByte(2)); + + resultSet.next(); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getByte(2)); + + resultSet.next(); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getByte(2)); + + dropSpace("byte_vals"); + } + + @Test + void testGetShortValue() { + testHelper.executeLua( + "box.schema.space.create('short_vals', { format = " + + "{{name = 'id', type = 'integer'}, {name = 'byte_val', type = 'integer', is_nullable = true} }" + + "})", + "box.space.short_vals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.short_vals:insert{1, -32768}", + "box.space.short_vals:insert{2, 32767}", + "box.space.short_vals:insert{4, 0}", + "box.space.short_vals:insert{5, -1}", + "box.space.short_vals:insert{6, 12843}", + "box.space.short_vals:insert{7, -7294}", + "box.space.short_vals:insert{8, 34921}", + "box.space.short_vals:insert{9, -37123}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("short_vals", "pk")); + + resultSet.next(); + assertEquals(-32768, resultSet.getShort(2)); + + resultSet.next(); + assertEquals(32767, resultSet.getShort(2)); + + resultSet.next(); + assertEquals(0, resultSet.getShort(2)); + assertFalse(resultSet.isNull(2)); + + resultSet.next(); + assertEquals(-1, resultSet.getShort(2)); + + resultSet.next(); + assertEquals(12843, resultSet.getShort(2)); + + resultSet.next(); + assertEquals(-7294, resultSet.getShort(2)); + + resultSet.next(); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getShort(2)); + + resultSet.next(); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getShort(2)); + + dropSpace("short_vals"); + } + + @Test + void testGetIntValue() { + testHelper.executeLua( + "box.schema.space.create('int_vals', { format = " + + "{{name = 'id', type = 'integer'}, {name = 'int_vals', type = 'integer', is_nullable = true} }" + + "})", + "box.space.int_vals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.int_vals:insert{1, -2147483648}", + "box.space.int_vals:insert{2, 2147483647}", + "box.space.int_vals:insert{4, 0}", + "box.space.int_vals:insert{5, -134}", + "box.space.int_vals:insert{6, 589213}", + "box.space.int_vals:insert{7, -1234987}", + "box.space.int_vals:insert{8, 3897234258}", + "box.space.int_vals:insert{9, -2289123645}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("int_vals", "pk")); + + resultSet.next(); + assertEquals(-2147483648, resultSet.getInt(2)); + + resultSet.next(); + assertEquals(2147483647, resultSet.getInt(2)); + + resultSet.next(); + assertEquals(0, resultSet.getInt(2)); + assertFalse(resultSet.isNull(2)); + + resultSet.next(); + assertEquals(-134, resultSet.getInt(2)); + + resultSet.next(); + assertEquals(589213, resultSet.getInt(2)); + + resultSet.next(); + assertEquals(-1234987, resultSet.getInt(2)); + + resultSet.next(); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getInt(2)); + + resultSet.next(); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getInt(2)); + + dropSpace("int_vals"); + } + + @Test + void testGetLongValue() { + testHelper.executeLua( + "box.schema.space.create('long_vals', { format = " + + "{{name = 'id', type = 'integer'}, {name = 'long_val', type = 'integer', is_nullable = true} }" + + "})", + "box.space.long_vals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.long_vals:insert{1, -9223372036854775808LL}", + "box.space.long_vals:insert{2, 9223372036854775807LL}", + "box.space.long_vals:insert{3, 0LL}", + "box.space.long_vals:insert{4, -89123LL}", + "box.space.long_vals:insert{5, 2183428734598754LL}", + "box.space.long_vals:insert{6, -918989823492348843LL}", + "box.space.long_vals:insert{7, 18446744073709551615ULL}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("long_vals", "pk")); + + resultSet.next(); + assertEquals(-9223372036854775808L, resultSet.getLong(2)); + + resultSet.next(); + assertEquals(9223372036854775807L, resultSet.getLong(2)); + + resultSet.next(); + assertEquals(0, resultSet.getLong(2)); + assertFalse(resultSet.isNull(2)); + + resultSet.next(); + assertEquals(-89123, resultSet.getLong(2)); + + resultSet.next(); + assertEquals(2183428734598754L, resultSet.getLong(2)); + + resultSet.next(); + assertEquals(-918989823492348843L, resultSet.getLong(2)); + + resultSet.next(); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getLong(2)); + + dropSpace("long_vals"); + } + + @Test + void testGetFloatValue() { + testHelper.executeLua( + "box.schema.space.create('float_vals', { format = " + + "{{name = 'id', type = 'integer'}, {name = 'float_val', type = 'number', is_nullable = true} }" + + "})", + "box.space.float_vals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.float_vals:insert{1, -12.6}", + "box.space.float_vals:insert{2, 14.098}", + "box.space.float_vals:insert{3, 0}", + "box.space.float_vals:insert{4, -1230988}", + "box.space.float_vals:insert{5, 2138562176}", + "box.space.float_vals:insert{6, -78.0}", + "box.space.float_vals:insert{7, 97.14827}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("float_vals", "pk")); + + resultSet.next(); + assertEquals(-12.6f, resultSet.getFloat(2)); + + resultSet.next(); + assertEquals(14.098f, resultSet.getFloat(2)); + + resultSet.next(); + assertEquals(0, resultSet.getFloat(2)); + + resultSet.next(); + assertEquals(-1230988.0f, resultSet.getFloat(2)); + + resultSet.next(); + assertEquals(2138562176.0f, resultSet.getFloat(2)); + + resultSet.next(); + assertEquals(-78.0f, resultSet.getFloat(2)); + + resultSet.next(); + assertEquals(97.14827f, resultSet.getFloat(2)); + + dropSpace("float_vals"); + } + + @Test + void testGetDoubleValue() { + testHelper.executeLua( + "box.schema.space.create('double_vals', { format = " + + "{{name = 'id', type = 'integer'}, {name = 'double_val', type = 'number', is_nullable = true} }" + + "})", + "box.space.double_vals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.double_vals:insert{1, -12.60}", + "box.space.double_vals:insert{2, 43.9093}", + "box.space.double_vals:insert{3, 0}", + "box.space.double_vals:insert{4, -89234234}", + "box.space.double_vals:insert{5, 532982423}", + "box.space.double_vals:insert{6, -134.0}", + "box.space.double_vals:insert{7, 4232.8264286}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("double_vals", "pk")); + + resultSet.next(); + assertEquals(-12.60, resultSet.getDouble(2)); + + resultSet.next(); + assertEquals(43.9093, resultSet.getDouble(2)); + + resultSet.next(); + assertEquals(0, resultSet.getDouble(2)); + + resultSet.next(); + assertEquals(-89234234, resultSet.getDouble(2)); + + resultSet.next(); + assertEquals(532982423, resultSet.getDouble(2)); + + resultSet.next(); + assertEquals(-134.0f, resultSet.getDouble(2)); + + resultSet.next(); + assertEquals(4232.8264286, resultSet.getDouble(2)); + + dropSpace("double_vals"); + } + + @Test + void testGetBooleanValue() { + testHelper.executeLua( + "box.schema.space.create('bool_vals', { format = " + + "{{name = 'id', type = 'integer'}, {name = 'bool_val', type = 'boolean', is_nullable = true} }" + + "})", + "box.space.bool_vals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.bool_vals:insert{1, true}", + "box.space.bool_vals:insert{2, false}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("bool_vals", "pk")); + + resultSet.next(); + assertTrue(resultSet.getBoolean(2)); + + resultSet.next(); + assertFalse(resultSet.getBoolean(2)); + + dropSpace("bool_vals"); + } + + @Test + void testGetBooleanFromNumber() { + testHelper.executeLua( + "box.schema.space.create('bool_vals', { format = " + + "{{name = 'id', type = 'integer'}, {name = 'bool_val', type = 'number', is_nullable = true} }" + + "})", + "box.space.bool_vals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.bool_vals:insert{1, 0}", + "box.space.bool_vals:insert{2, 1}", + "box.space.bool_vals:insert{3, 1.0}", + "box.space.bool_vals:insert{4, 0.0}", + "box.space.bool_vals:insert{5, -0.0}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("bool_vals", "pk")); + + resultSet.next(); + assertFalse(resultSet.getBoolean(2)); + + resultSet.next(); + assertTrue(resultSet.getBoolean(2)); + + resultSet.next(); + assertTrue(resultSet.getBoolean(2)); + + resultSet.next(); + assertFalse(resultSet.getBoolean(2)); + + resultSet.next(); + assertFalse(resultSet.getBoolean(2)); + + dropSpace("bool_vals"); + } + + @Test + void testGetBooleanFromString() { + testHelper.executeLua( + "box.schema.space.create('bool_vals', { format = " + + "{{name = 'id', type = 'integer'}, {name = 'bool_val', type = 'string', is_nullable = true} }" + + "})", + "box.space.bool_vals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.bool_vals:insert{1, '0'}", + "box.space.bool_vals:insert{2, '1'}", + "box.space.bool_vals:insert{3, 'true'}", + "box.space.bool_vals:insert{4, 'false'}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("bool_vals", "pk")); + + resultSet.next(); + assertFalse(resultSet.getBoolean(2)); + + resultSet.next(); + assertTrue(resultSet.getBoolean(2)); + + resultSet.next(); + assertTrue(resultSet.getBoolean(2)); + + resultSet.next(); + assertFalse(resultSet.getBoolean(2)); + + dropSpace("bool_vals"); + } + + @Test + void testGetBytesValue() { + testHelper.executeLua( + "box.schema.space.create('bin_vals', { format = " + + "{{name = 'id', type = 'integer'}, {name = 'bin_val', type = 'scalar', is_nullable = true} }})", + "box.space.bin_vals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.bin_vals:insert{1, [[some text]]}", + "box.space.bin_vals:insert{2, '\\01\\02\\03\\04\\05'}", + "box.space.bin_vals:insert{3, 12}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("bin_vals", "pk")); + + resultSet.next(); + assertArrayEquals("some text".getBytes(StandardCharsets.UTF_8), resultSet.getBytes(2)); + + resultSet.next(); + assertArrayEquals(new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5 }, resultSet.getBytes(2)); + + resultSet.next(); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getBytes(2)); + + dropSpace("bin_vals"); + } + + @Test + void testGetStringValue() { + testHelper.executeLua( + "box.schema.space.create('string_vals', { format = " + + "{{name = 'id', type = 'integer'}, {name = 'text_val', type = 'string', is_nullable = true} }" + + "})", + "box.space.string_vals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.string_vals:insert{1, 'some text'}", + "box.space.string_vals:insert{2, 'word'}", + "box.space.string_vals:insert{3, ''}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("string_vals", "pk")); + + resultSet.next(); + assertEquals("some text", resultSet.getString(2)); + + resultSet.next(); + assertEquals("word", resultSet.getString(2)); + + resultSet.next(); + assertEquals("", resultSet.getString(2)); + + dropSpace("string_vals"); + } + + @Test + void testGetWrongStringValue() { + TarantoolResultSet resultSet = client.executeRequest(Requests.evalRequest( + "return {a=1,b=2}, {1,2,3}" + )); + + resultSet.next(); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getString(1)); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getString(2)); + } + + @Test + void testGetStringFromScalar() { + testHelper.executeLua( + "box.schema.space.create('scalar_vals', { format = " + + "{{name = 'id', type = 'integer'}, {name = 'scalar_val', type = 'scalar', is_nullable = true} }" + + "})", + "box.space.scalar_vals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.scalar_vals:insert{1, 'some text'}", + "box.space.scalar_vals:insert{2, 12}", + "box.space.scalar_vals:insert{3, 12.45}", + "box.space.scalar_vals:insert{4, '\\01\\02\\03'}", + "box.space.scalar_vals:insert{5, true}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("scalar_vals", "pk")); + + resultSet.next(); + assertEquals("some text", resultSet.getString(2)); + + resultSet.next(); + assertEquals("12", resultSet.getString(2)); + + resultSet.next(); + assertEquals("12.45", resultSet.getString(2)); + + resultSet.next(); + assertEquals(new String(new byte[] { 0x1, 0x2, 0x3 }), resultSet.getString(2)); + + resultSet.next(); + assertEquals("true", resultSet.getString(2)); + + dropSpace("scalar_vals"); + } + + @Test + void testGetObject() { + testHelper.executeLua( + "box.schema.space.create('object_vals', { format = " + + "{{name = 'id', type = 'integer'}, {name = 'object_val', type = 'scalar', is_nullable = true} }" + + "})", + "box.space.object_vals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.object_vals:insert{1, 'some text'}", + "box.space.object_vals:insert{2, 12}", + "box.space.object_vals:insert{3, 12.45}", + "box.space.object_vals:insert{4, '\\01\\02\\03'}", + "box.space.object_vals:insert{5, true}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("object_vals", "pk")); + + resultSet.next(); + assertEquals("some text", resultSet.getObject(2)); + + resultSet.next(); + assertEquals(12, resultSet.getObject(2)); + + resultSet.next(); + assertEquals(12.45, (double) resultSet.getObject(2)); + + resultSet.next(); + assertEquals(new String(new byte[] { 0x1, 0x2, 0x3 }), resultSet.getObject(2)); + + resultSet.next(); + assertTrue((boolean) resultSet.getObject(2)); + + dropSpace("object_vals"); + } + + @Test + void testGetBigInteger() { + testHelper.executeLua( + "box.schema.space.create('bigint_vals', { format = " + + "{{name = 'id', type = 'integer'}, {name = 'bigint_val', type = 'integer', is_nullable = true} }" + + "})", + "box.space.bigint_vals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.bigint_vals:insert{1, 10001}", + "box.space.bigint_vals:insert{2, 12}", + "box.space.bigint_vals:insert{3, 0}", + "box.space.bigint_vals:insert{4, 18446744073709551615ULL}", + "box.space.bigint_vals:insert{5, -4792}", + "box.space.bigint_vals:insert{6, -9223372036854775808LL}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("bigint_vals", "pk")); + + resultSet.next(); + assertEquals(new BigInteger("10001"), resultSet.getBigInteger(2)); + + resultSet.next(); + assertEquals(new BigInteger("12"), resultSet.getBigInteger(2)); + + resultSet.next(); + assertEquals(new BigInteger("0"), resultSet.getBigInteger(2)); + + resultSet.next(); + assertEquals(new BigInteger("18446744073709551615"), resultSet.getBigInteger(2)); + + resultSet.next(); + assertEquals(new BigInteger("-4792"), resultSet.getBigInteger(2)); + + resultSet.next(); + assertEquals(new BigInteger("-9223372036854775808"), resultSet.getBigInteger(2)); + + dropSpace("bigint_vals"); + } + + @Test + void testGetTuple() { + testHelper.executeLua( + "box.schema.space.create('animals', { format = " + + "{{name = 'id', type = 'integer'}, " + + " {name = 'name', type = 'string'}," + + " {name = 'is_predator', type = 'boolean', is_nullable = true}}" + + "})", + "box.space.animals:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.animals:insert{1, 'Zebra', false}", + "box.space.animals:insert{2, 'Lion', true}", + "box.space.animals:insert{3, 'Monkey', nil}", + "box.space.animals:insert{4, 'Wolf', true}", + "box.space.animals:insert{5, 'Duck', nil}", + "box.space.animals:insert{6, 'Raccoon', nil}" + ); + + int[] rowSizes = {3, 3, 2, 3, 2, 2}; + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("animals", "pk")); + int rowNumber = 0; + while (resultSet.next()) { + TarantoolTuple tuple = resultSet.getTuple(TarantoolResultSet.ACTUAL_ROW_SIZE); + assertEquals(rowSizes[rowNumber++], tuple.size()); + + tuple = resultSet.getTuple(3); + assertEquals(3, tuple.size()); + + tuple = resultSet.getTuple(4); + assertEquals(4, tuple.size()); + assertNull(tuple.get(4)); + } + + dropSpace("animals"); + } + + @Test + void testGetTupleFromSizedSpace() { + testHelper.executeLua( + "box.schema.space.create('movies', {field_count = 4, format = " + + "{{name = 'id', type = 'integer'}," + + " {name = 'name', type = 'string'}," + + " {name = 'genre', type = 'string', is_nullable = true}}" + + "})", + "box.space.movies:create_index('pk', { type = 'TREE', parts = {'id'} } )", + + "box.space.movies:insert{1, 'The Godfather', 'Drama', box.NULL}", + "box.space.movies:insert{2, 'The Wizard of Oz', 'Fantasy', box.NULL}", + "box.space.movies:insert{3, 'The Shawshank Redemption', nil, box.NULL}", + "box.space.movies:insert{4, 'Pulp Fiction', 'Crime', box.NULL}", + "box.space.movies:insert{5, '2001: A Space Odyssey', nil, box.NULL}", + "box.space.movies:insert{6, 'Forrest Gump', nil, box.NULL}" + ); + + TarantoolResultSet resultSet = client.executeRequest(Requests.selectRequest("movies", "pk")); + while (resultSet.next()) { + TarantoolTuple tuple = resultSet.getTuple(TarantoolResultSet.ACTUAL_ROW_SIZE); + assertEquals(4, tuple.size()); + + tuple = resultSet.getTuple(4); + assertEquals(4, tuple.size()); + } + + dropSpace("movies"); + } + + @Test + void testRequestBoundTuple() { + TarantoolResultSet resultSet = client.executeRequest( + Requests.evalRequest( + "return 'a', 'b', 'c', 'd', 'e'" + ) + ); + resultSet.next(); + assertThrows(IllegalArgumentException.class, () -> resultSet.getTuple(-10)); + assertThrows(IllegalArgumentException.class, () -> resultSet.getTuple(4)); + assertEquals(5, resultSet.getTuple(5).size()); + assertEquals(5, resultSet.getTuple(TarantoolResultSet.ACTUAL_ROW_SIZE).size()); + + TarantoolTuple tuple = resultSet.getTuple(100); + assertEquals(100, tuple.size()); + assertEquals(100, tuple.toList().size()); + assertEquals(100, tuple.toArray().length); + String[] values = {"a", "b", "c", "d", "e"}; + for (int i = 1; i <= 5; i++) { + assertEquals(values[i - 1], tuple.get(i)); + } + for (int i = 6; i <= 100; i++) { + assertNull(tuple.get(i)); + } + assertThrows(IndexOutOfBoundsException.class, () -> tuple.get(101)); + } + + @Test + void testGetList() { + TarantoolResultSet resultSet = client.executeRequest( + Requests.evalRequest("return {'a','b','c'}, {1,2,3}, nil, {}, 'string'") + ); + + resultSet.next(); + assertEquals(5, resultSet.getRowSize()); + assertEquals(Arrays.asList("a", "b", "c"), resultSet.getList(1)); + assertEquals(Arrays.asList(1, 2, 3), resultSet.getList(2)); + assertNull(resultSet.getList(3)); + assertEquals(Collections.emptyList(), resultSet.getList(4)); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getList(5)); + } + + @Test + void testGetMap() { + TarantoolResultSet resultSet = client.executeRequest( + Requests.evalRequest( + "return {a=1,b=2}, {['key 1']=8,['key 2']=9}, {['+']='add',['/']='div'}, 'string'" + ) + ); + + resultSet.next(); + assertEquals(4, resultSet.getRowSize()); + assertEquals( + new HashMap() { + { + put("a", 1); + put("b", 2); + } + }, + resultSet.getMap(1)); + assertEquals( + new HashMap() { + { + put("key 1", 8); + put("key 2", 9); + } + }, + resultSet.getMap(2)); + assertEquals( + new HashMap() { + { + put("+", "add"); + put("/", "div"); + } + }, + resultSet.getMap(3)); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getMap(4)); + } + + @Test + void testGetMixedValues() { + testHelper.executeLua( + "function getMixed() return 10, -20.5, 'string', nil, true, {1,2,3}, {x='f',y='t'} end" + ); + TarantoolResultSet resultSet = client.executeRequest(Requests.callRequest("getMixed")); + + resultSet.next(); + assertEquals(7, resultSet.getRowSize()); + assertEquals(10, resultSet.getInt(1)); + assertEquals(-20.5, resultSet.getDouble(2)); + assertEquals("string", resultSet.getString(3)); + assertTrue(resultSet.isNull(4)); + assertTrue(resultSet.getBoolean(5)); + assertEquals(Arrays.asList(1, 2, 3), resultSet.getList(6)); + assertEquals( + new HashMap() { + { + put("x", "f"); + put("y", "t"); + } + }, + resultSet.getMap(7)); + } + + @Test + void testGetNullValues() { + testHelper.executeLua( + "function getNull() return nil end" + ); + TarantoolResultSet resultSet = client.executeRequest(Requests.callRequest("getNull")); + + resultSet.next(); + assertFalse(resultSet.getBoolean(1)); + assertEquals(0, resultSet.getByte(1)); + assertEquals(0, resultSet.getShort(1)); + assertEquals(0, resultSet.getInt(1)); + assertEquals(0, resultSet.getLong(1)); + assertNull(resultSet.getBigInteger(1)); + assertEquals(0.0, resultSet.getFloat(1)); + assertEquals(0.0, resultSet.getDouble(1)); + assertNull(resultSet.getList(1)); + assertNull(resultSet.getMap(1)); + assertNull(resultSet.getObject(1)); + assertNull(resultSet.getString(1)); + assertNull(resultSet.getBytes(1)); + } + + @Test + void testGetFloatNumberFromString() { + testHelper.executeLua( + "function getString() return '120.5' end" + ); + TarantoolResultSet resultSet = client.executeRequest(Requests.callRequest("getString")); + + resultSet.next(); + assertEquals(120.5f, resultSet.getFloat(1)); + assertEquals(120.5d, resultSet.getDouble(1)); + } + + void testGetIntegerNumberFromString() { + testHelper.executeLua( + "function getString() return '120' end" + ); + TarantoolResultSet resultSet = client.executeRequest(Requests.callRequest("getString")); + + resultSet.next(); + assertEquals(120, resultSet.getByte(1)); + assertEquals(120, resultSet.getShort(1)); + assertEquals(120, resultSet.getInt(1)); + assertEquals(120, resultSet.getLong(1)); + assertEquals(BigInteger.valueOf(120), resultSet.getBigInteger(1)); + } + + + @Test + void testGetNotParsableNumberFromString() { + testHelper.executeLua( + "function getString() return 'five point six' end" + ); + TarantoolResultSet resultSet = client.executeRequest(Requests.callRequest("getString")); + + resultSet.next(); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getByte(1)); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getShort(1)); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getInt(1)); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getLong(1)); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getBigInteger(1)); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getFloat(1)); + assertThrows(NotConvertibleValueException.class, () -> resultSet.getDouble(1)); + } + + @Test + void testGetTooLargeNumberValue() { + testHelper.executeLua( + "function getNumber() return 300, 100000, 5000000000, 10000000000000000000ULL end", + "function getString() return '300', '100000', '5000000000', '10000000000000000000' end" + ); + TarantoolResultSet numberResult = client.executeRequest(Requests.callRequest("getNumber")); + + numberResult.next(); + assertThrows(NotConvertibleValueException.class, () -> numberResult.getByte(1)); + assertThrows(NotConvertibleValueException.class, () -> numberResult.getShort(2)); + assertThrows(NotConvertibleValueException.class, () -> numberResult.getInt(3)); + assertThrows(NotConvertibleValueException.class, () -> numberResult.getLong(4)); + + TarantoolResultSet stringResult = client.executeRequest(Requests.callRequest("getString")); + + stringResult.next(); + assertThrows(NotConvertibleValueException.class, () -> stringResult.getByte(1)); + assertThrows(NotConvertibleValueException.class, () -> stringResult.getShort(2)); + assertThrows(NotConvertibleValueException.class, () -> stringResult.getInt(3)); + assertThrows(NotConvertibleValueException.class, () -> stringResult.getLong(4)); + } + + private void dropSpace(String spaceName) { + testHelper.executeLua(String.format("box.space.%1$s and box.space.%1$s:drop()", spaceName)); + } +} diff --git a/src/test/java/org/tarantool/conversion/DefaultConverterRegistryTest.java b/src/test/java/org/tarantool/conversion/DefaultConverterRegistryTest.java new file mode 100644 index 00000000..0d222be7 --- /dev/null +++ b/src/test/java/org/tarantool/conversion/DefaultConverterRegistryTest.java @@ -0,0 +1,785 @@ +package org.tarantool.conversion; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.tarantool.conversion.DefaultConverterRegistryTest.ConversionTypesTester.testTypesForRegistry; +import static org.tarantool.conversion.DefaultConverterRegistryTest.ConversionValuesTester.testValuesForRegistry; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * Tests supported converters for {@link DefaultConverterRegistry}. + */ +class DefaultConverterRegistryTest { + + private ConverterRegistry converterRegistry; + + @BeforeEach + void setUp() { + converterRegistry = new DefaultConverterRegistry(); + } + + @Test + void testIsConvertibleToByte() { + testTypesForRegistry(converterRegistry) + .from( + Short.class, Integer.class, Long.class, Float.class, + Double.class, BigInteger.class, BigDecimal.class, String.class + ) + .notFrom(Boolean.class, byte[].class) + .to(Byte.class) + .assertTypes(); + } + + @Test + void testToByteConversions() { + TestablePair[] pairs = { + new TestablePair((short) 127, (byte) 127), new TestablePair((short) -128, (byte) -128), + new TestablePair(127, (byte) 127), new TestablePair(-128, (byte) -128), + new TestablePair(127L, (byte) 127), new TestablePair(-128L, (byte) -128), + + new TestablePair(127.0f, (byte) 127), new TestablePair(-128.0f, (byte) -128), + new TestablePair(127.0d, (byte) 127), new TestablePair(-128.0d, (byte) -128), + + new TestablePair(BigInteger.valueOf(127), (byte) 127), + new TestablePair(BigInteger.valueOf(-128), (byte) -128), + + new TestablePair(BigDecimal.valueOf(127.0), (byte) 127), + new TestablePair(BigDecimal.valueOf(-128.0), (byte) -128), + + new TestablePair("127", (byte) 127), + new TestablePair("-128", (byte) -128), + }; + + testValuesForRegistry(converterRegistry) + .fromValues(pairs) + .to(Byte.class) + .assertValues(); + } + + @Test + void testTooLargeNumbersToByteConversions() { + Object[] unsupportedValues = { + (short) 128, (short) -129, + 128, -129, + 128L, -129L, + 128.0f, -129.0f, 127.5f, -128.5f, 30.125f, + 128.0d, -129.0d, 127.5d, -128.5d, 60.25d, + BigInteger.valueOf(128), BigInteger.valueOf(-129), + BigDecimal.valueOf(128.0), BigDecimal.valueOf(-129.0), + BigDecimal.valueOf(127.5), BigDecimal.valueOf(-128.5), BigDecimal.valueOf(12.4), + "128", "-129", "127.0", "127.5", + }; + + testValuesForRegistry(converterRegistry) + .fromNotConvertibleValues(unsupportedValues) + .to(Byte.class) + .assertNotConvertible(); + } + + @Test + void testIsConvertibleToShort() { + testTypesForRegistry(converterRegistry) + .from( + Byte.class, Integer.class, Long.class, Float.class, + Double.class, BigInteger.class, BigDecimal.class, String.class + ) + .notFrom(Boolean.class, byte[].class) + .to(Short.class) + .assertTypes(); + } + + @Test + void testToShortConversions() { + TestablePair[] pairs = { + new TestablePair((byte) 127, (short) 127), new TestablePair((byte) -128, (short) -128), + new TestablePair(32767, (short) 32767), new TestablePair(-32768, (short) -32768), + new TestablePair(32767L, (short) 32767), new TestablePair(-32768L, (short) -32768), + + new TestablePair(32767.0f, (short) 32767), new TestablePair(-32768.0f, (short) -32768), + new TestablePair(32767.0d, (short) 32767), new TestablePair(-32768.0d, (short) -32768), + + new TestablePair(BigInteger.valueOf(32767), (short) 32767), + new TestablePair(BigInteger.valueOf(-32768), (short) -32768), + + new TestablePair(BigDecimal.valueOf(32767.0), (short) 32767), + new TestablePair(BigDecimal.valueOf(-32768.0), (short) -32768), + + new TestablePair("32767", (short) 32767), + new TestablePair("-32768", (short) -32768), + }; + + testValuesForRegistry(converterRegistry) + .fromValues(pairs) + .to(Short.class) + .assertValues(); + } + + @Test + void testTooLargeNumbersToShortConversions() { + Object[] unsupportedValues = { + 32768, -32769, + 32768L, -32769L, + 32768.0f, -32769.0f, 32767.4f, -32768.5f, 650.8f, + 32768.0d, -32769.0d, 32767.3d, -32768.5d, 125.7d, + BigInteger.valueOf(32768), BigInteger.valueOf(-32769), + BigDecimal.valueOf(32768.0), BigDecimal.valueOf(-32769.0), + BigDecimal.valueOf(32767.4), BigDecimal.valueOf(-32768.5), BigDecimal.valueOf(100.7), + "32768", "-32769", "32768.0", "32768.5", + }; + + testValuesForRegistry(converterRegistry) + .fromNotConvertibleValues(unsupportedValues) + .to(Byte.class) + .assertNotConvertible(); + } + + @Test + void testIsConvertibleToInt() { + testTypesForRegistry(converterRegistry) + .from( + Byte.class, Short.class, Long.class, Float.class, + Double.class, BigInteger.class, BigDecimal.class, String.class + ) + .notFrom(Boolean.class, byte[].class) + .to(Integer.class) + .assertTypes(); + } + + @Test + void testToIntConversions() { + TestablePair[] pairs = { + new TestablePair((byte) 127, 127), new TestablePair((byte) -128, -128), + new TestablePair((short) 32767, 32767), new TestablePair((short) -32768, -32768), + new TestablePair(2147483647L, 2147483647), new TestablePair(-2147483648L, -2147483648), + + // (equal to Float.intBitsToFloat(0x4EFFFFFF) - max integral float within 2^30..2^31-1 range) + new TestablePair(2147483520.0f, 2147483520), + new TestablePair(-2147483648.0f, -2147483648), + + new TestablePair(2147483647.0d, 2147483647), + new TestablePair(-2147483648.0d, -2147483648), + + new TestablePair(BigInteger.valueOf(2147483647), 2147483647), + new TestablePair(BigInteger.valueOf(-2147483648), -2147483648), + + new TestablePair(BigDecimal.valueOf(2147483647.0), 2147483647), + new TestablePair(BigDecimal.valueOf(-2147483648.0), -2147483648), + + new TestablePair("2147483647", 2147483647), + new TestablePair("-2147483648", -2147483648), + }; + + testValuesForRegistry(converterRegistry) + .fromValues(pairs) + .to(Integer.class) + .assertValues(); + } + + @Test + void testTooLargeNumbersToIntConversions() { + Object[] unsupportedValues = { + 2147483648L, -2147483649L, + 120.6f, 2147483648.0f, + 150.6d, 2147483648.0d, -2147483649.0d, 2147483647.5d, -2147483648.5d, + BigInteger.valueOf(2147483648L), BigInteger.valueOf(-2147483649L), + BigDecimal.valueOf(560.5), BigDecimal.valueOf(2147483648.0), BigDecimal.valueOf(-2147483649.0), + BigDecimal.valueOf(2147483647.5), BigDecimal.valueOf(-2147483648.5d), + "2147483648", "-2147483649", "2147483647.0", "-2147483648.0", "102.6" + }; + + testValuesForRegistry(converterRegistry) + .fromNotConvertibleValues(unsupportedValues) + .to(Integer.class) + .assertNotConvertible(); + } + + @Test + void testIsConvertibleToLong() { + testTypesForRegistry(converterRegistry) + .from( + Byte.class, Short.class, Integer.class, Float.class, + Double.class, BigInteger.class, BigDecimal.class, String.class + ) + .notFrom(Boolean.class, byte[].class) + .to(Long.class) + .assertTypes(); + } + + @Test + void testToLongConversions() { + TestablePair[] pairs = { + new TestablePair((byte) 127, 127L), new TestablePair((byte) -128, -128L), + new TestablePair((short) 32767, 32767L), new TestablePair((short) -32768, -32768L), + new TestablePair(2147483647, 2147483647L), new TestablePair(-2147483648, -2147483648L), + + // (equal to Float.intBitsToFloat(0x5EFFFFFF) - max integral float within 2^62..2^63-1 range) + new TestablePair(9223371487098961920.0f, 9223371487098961920L), + new TestablePair(-9223372036854775808.0f, -9223372036854775808L), + + // (equal to Double.longBitsToDouble(0x43DFFFFFFFFFFFFFL) - max integral double within 2^62..2^63-1 range) + new TestablePair(9223372036854774784.0d, 9223372036854774784L), + new TestablePair(-9223372036854775808.0d, -9223372036854775808L), + + new TestablePair(BigInteger.valueOf(9223372036854775807L), 9223372036854775807L), + new TestablePair(BigInteger.valueOf(-9223372036854775808L), -9223372036854775808L), + + new TestablePair(new BigDecimal("9223372036854775807.0"), 9223372036854775807L), + new TestablePair(new BigDecimal("-9223372036854775808.0"), -9223372036854775808L), + + new TestablePair("9223372036854775807", 9223372036854775807L), + new TestablePair("-9223372036854775808", -9223372036854775808L), + }; + + testValuesForRegistry(converterRegistry) + .fromValues(pairs) + .to(Long.class) + .assertValues(); + } + + @Test + void testTooLargeNumbersToLongConversions() { + Object[] unsupportedValues = { + 9223372036854775808.0f, + 12.6f, 18_446_744_073_709_551_616.0f, -18_446_744_073_709_551_616.0f, + 12.6d, 18_446_744_073_709_551_616.0d, -18_446_744_073_709_551_616.0d, + new BigInteger("9223372036854775808"), new BigInteger("-9223372036854775809"), + new BigDecimal("9223372036854775808.0"), new BigDecimal("-9223372036854775809.0"), + new BigDecimal("12.6"), new BigDecimal("-34.2"), + "9223372036854775808", "-9223372036854775809", "12.6" + }; + + testValuesForRegistry(converterRegistry) + .fromNotConvertibleValues(unsupportedValues) + .to(Long.class) + .assertNotConvertible(); + } + + @Test + void testIsConvertibleToFloat() { + testTypesForRegistry(converterRegistry) + .from( + Byte.class, Short.class, Integer.class, Long.class, + Double.class, BigInteger.class, BigDecimal.class, String.class + ) + .notFrom(Boolean.class, byte[].class) + .to(Float.class) + .assertTypes(); + } + + @Test + void testToFloatConversions() { + TestablePair[] pairs = { + new TestablePair((byte) 127, 127.0f), new TestablePair((byte) -128, -128.0f), + new TestablePair((short) 32767, 32767.0f), new TestablePair((short) -32768, -32768.0f), + // max int cannot be accommodated exactly, + // the last int less than max int being exactly representable is 2147483520 + // (equal to Float.intBitsToFloat(0x4EFFFFFF) - max integral float within 2^30..2^31-1 range) + new TestablePair(2147483520, 2147483520.0f), + // the pen-ultimate int less than max int being exactly representable is 2147483392 + // (equal to Float.intBitsToFloat(0x4EFFFFFE) + new TestablePair(2147483392, 2147483392.0f), + new TestablePair(-2147483648, -2147483648.0f), + + new TestablePair(2147483520L, 2147483520.0f), + // (equal to Float.intBitsToFloat(0x5EFFFFFF) - max integral float within 2^62..2^63-1 range) + new TestablePair(9223371487098961920L, 9223371487098961920.0f), + new TestablePair(-9223372036854775808L, -9223372036854775808.0f), + + new TestablePair(2147483648.0d, 2147483648.0f), + new TestablePair(-2147483648.0d, -2147483648.0f), + + new TestablePair(new BigInteger("18446744073709551616"), 18446744073709551616.0f), + // (equal to Float.intBitsToFloat(0x5F7FFFFF) - max integral float within 2^63..2^64-1 range) + new TestablePair(new BigInteger("18446742974197923840"), 18446742974197923840.0f), + new TestablePair(new BigInteger("-18446744073709551616"), -18446744073709551616.0f), + + new TestablePair(new BigDecimal("18446744073709551616.0"), 18446744073709551616.0f), + new TestablePair(new BigDecimal("-18446744073709551616.0"), -18446744073709551616.0f), + + new TestablePair("9223372036854775808", 9223372036854775808.0f), + new TestablePair("-9223372036854775808", -9223372036854775808.0f), + }; + + testValuesForRegistry(converterRegistry) + .fromValues(pairs) + .to(Float.class) + .assertValues(); + } + + @Test + void testTooLargeNumbersToFloatConversions() { + Object[] unsupportedValues = { + // numbers that have more than 24 significant bits + 2147483647, -2147483647, + 9223372036854775807L, -9223372036854775807L, + new BigInteger("18446744073709551615"), new BigInteger("-18446744073709551615"), + // more than upper bound of float 2^127 * (2 - 2^-23) + BigInteger.valueOf(2).pow(128), + "six" + }; + + testValuesForRegistry(converterRegistry) + .fromNotConvertibleValues(unsupportedValues) + .to(Float.class) + .assertNotConvertible(); + } + + @Test + void name() { + converterRegistry.convert(new BigInteger("18446744073709549568"), Double.class); + converterRegistry.convert(new BigInteger("18446742974197923840"), Float.class); + converterRegistry.convert(9223371487098961920L, Float.class); + } + + @Test + void testIsConvertibleToDouble() { + testTypesForRegistry(converterRegistry) + .from( + Byte.class, Short.class, Integer.class, Long.class, + Float.class, BigInteger.class, BigDecimal.class, String.class + ) + .notFrom(Boolean.class, byte[].class) + .to(Double.class) + .assertTypes(); + } + + @Test + void testToDoubleConversions() { + TestablePair[] pairs = { + new TestablePair((byte) 127, 127.0), new TestablePair((byte) -128, -128.0), + new TestablePair((short) 32767, 32767.0), new TestablePair((short) -32768, -32768.0), + new TestablePair(2147483647, 2147483647.0), new TestablePair(-2147483648, -2147483648.0), + + // max long cannot be accommodated exactly, + // the last long less than max int being exactly representable is 9223372036854774784 + // (equal to Double.longBitsToDouble(0x43DFFFFFFFFFFFFFL) - max integral double within 2^62..2^63-1 range) + new TestablePair(9223372036854774784L, 9223372036854774784.0), + // the pen-ultimate long less than max long being exactly representable is 9223372036854773760 + // (equal to Double.longBitsToDouble(0x43DFFFFFFFFFFFFEL) + new TestablePair(9223372036854773760L, 9223372036854773760.0), + + new TestablePair(-9223372036854775808L, -9223372036854775808.0), + + new TestablePair(2147483648.0f, 2147483648.0), + new TestablePair(-2147483648.0f, -2147483648.0), + + new TestablePair(new BigInteger("18446744073709551616"), 18446744073709551616.0), + new TestablePair(new BigInteger("-18446744073709551616"), -18446744073709551616.0), + // (equal to Double.longBitsToDouble(0x43EFFFFFFFFFFFFFL) - max integral double within 2^63..2^64-1 range) + new TestablePair(new BigInteger("18446744073709549568"), 18446744073709549568.0), + + new TestablePair(new BigDecimal("18446744073709551616.0"), 18446744073709551616.0), + new TestablePair(new BigDecimal("-18446744073709551616.0"), -18446744073709551616.0), + new TestablePair(new BigDecimal("32767.5"), 32767.5), + new TestablePair(new BigDecimal("0.0"), 0.0), + + new TestablePair("9223372036854775808", 9223372036854775808.0), + new TestablePair("-9223372036854775808", -9223372036854775808.0), + new TestablePair("64.5", 64.5), + }; + + testValuesForRegistry(converterRegistry) + .fromValues(pairs) + .to(Double.class) + .assertValues(); + } + + @Test + void testUnsupportedToDoubleConversions() { + Object[] unsupportedValues = { + // numbers that have more than 53 significant bits + 9223372036854775807L, -9223372036854775807L, + new BigInteger("18446744073709551615"), new BigInteger("-18446744073709551615"), + // more than upper bound of double 2^1023 * (2 - 2^-52) + BigInteger.valueOf(2).pow(1024), + "six" + }; + + testValuesForRegistry(converterRegistry) + .fromNotConvertibleValues(unsupportedValues) + .to(Double.class) + .assertNotConvertible(); + } + + @Test + void testIsConvertibleToBoolean() { + testTypesForRegistry(converterRegistry) + .from( + Byte.class, Short.class, Integer.class, Long.class, + Float.class, Double.class, BigInteger.class, BigDecimal.class, String.class + ) + .notFrom(byte[].class) + .to(Boolean.class) + .assertTypes(); + } + + @Test + void testToBooleanConversions() { + TestablePair[] pairs = { + new TestablePair((byte) 0, false), new TestablePair((byte) 1, true), + new TestablePair((short) 0, false), new TestablePair((short) 1, true), + new TestablePair(0, false), new TestablePair(1, true), + new TestablePair(0L, false), new TestablePair(1L, true), + new TestablePair(-0.0f, false), new TestablePair(0.0f, false), + new TestablePair(1.0f, true), + new TestablePair(-0.0d, false), new TestablePair(0.0d, false), + new TestablePair(1.0d, true), + new TestablePair(BigInteger.ZERO, false), new TestablePair(BigInteger.ONE, true), + new TestablePair(BigDecimal.ZERO, false), new TestablePair(BigDecimal.ONE, true), + new TestablePair("false", false), new TestablePair("true", true), + new TestablePair("0", false), new TestablePair("1", true), + }; + + testValuesForRegistry(converterRegistry) + .fromValues(pairs) + .to(Boolean.class) + .assertValues(); + } + + @Test + void testUnsupportedToBooleanConversions() { + Object[] unsupportedValues = { + (byte) 2, 3, 4L, + 1.2f, 0.5f, 6.2f, Float.NEGATIVE_INFINITY, + 7.0d, 0.3d, 1.2d, Double.POSITIVE_INFINITY, + new BigInteger("8"), new BigDecimal("9.5"), + "f", "t", "y", "n", "on", "off", "yes", "no", " 1", "0 " + }; + + testValuesForRegistry(converterRegistry) + .fromNotConvertibleValues(unsupportedValues) + .to(Boolean.class) + .assertNotConvertible(); + } + + @Test + void testIsConvertibleToBigInteger() { + testTypesForRegistry(converterRegistry) + .from( + Byte.class, Short.class, Integer.class, Long.class, + Float.class, Double.class, BigDecimal.class, String.class + ) + .notFrom(Boolean.class, byte[].class) + .to(BigInteger.class) + .assertTypes(); + } + + @Test + void testToBigIntegerConversions() { + TestablePair[] pairs = { + new TestablePair((byte) -128, BigInteger.valueOf(-128)), + new TestablePair((byte) 127, BigInteger.valueOf(127)), + new TestablePair((short) 32767, BigInteger.valueOf(32767)), + new TestablePair((short) -32768, BigInteger.valueOf(-32768)), + new TestablePair(2147483647, BigInteger.valueOf(2147483647)), + new TestablePair(-2147483648, BigInteger.valueOf(-2147483648)), + new TestablePair(9223372036854775807L, BigInteger.valueOf(9223372036854775807L)), + new TestablePair(-9223372036854775808L, BigInteger.valueOf(-9223372036854775808L)), + new TestablePair(-0.0f, BigInteger.ZERO), + new TestablePair(0.0f, BigInteger.ZERO), + new TestablePair(18_446_744_073_709_551_616.0f, new BigInteger("18446744073709551616")), + new TestablePair(-0.0d, BigInteger.ZERO), + new TestablePair(0.0d, BigInteger.ZERO), + new TestablePair(18_446_744_073_709_551_616.0d, new BigInteger("18446744073709551616")), + new TestablePair("18446744073709551615", new BigInteger("18446744073709551615")), + }; + + testValuesForRegistry(converterRegistry) + .fromValues(pairs) + .to(BigInteger.class) + .assertValues(); + } + + @Test + void testUnsupportedToBigIntegerConversions() { + Object[] unsupportedValues = { + 65.5f, + 65.5f, + Float.NaN, + Float.POSITIVE_INFINITY, + 650.5d, + Double.NaN, + Double.NEGATIVE_INFINITY, + BigDecimal.valueOf(165.5), + "12.5", + "four" + }; + + testValuesForRegistry(converterRegistry) + .fromNotConvertibleValues(unsupportedValues) + .to(BigInteger.class) + .assertNotConvertible(); + } + + @Test + void testIsConvertibleToBigDecimal() { + testTypesForRegistry(converterRegistry) + .from( + Byte.class, Short.class, Integer.class, Long.class, + Float.class, Double.class, BigInteger.class, String.class + ) + .notFrom(Boolean.class, byte[].class) + .to(BigDecimal.class) + .assertTypes(); + } + + @Test + void testToBigDecimalConversions() { + TestablePair[] pairs = { + new TestablePair((byte) -128, BigDecimal.valueOf(-128)), + new TestablePair((byte) 127, BigDecimal.valueOf(127)), + new TestablePair((short) 32767, BigDecimal.valueOf(32767)), + new TestablePair((short) -32768, BigDecimal.valueOf(-32768)), + new TestablePair(2147483647, BigDecimal.valueOf(2147483647)), + new TestablePair(-2147483648, BigDecimal.valueOf(-2147483648)), + new TestablePair(9223372036854775807L, BigDecimal.valueOf(9223372036854775807L)), + new TestablePair(-9223372036854775808L, BigDecimal.valueOf(-9223372036854775808L)), + new TestablePair(12.5f, BigDecimal.valueOf(12.5f)), + new TestablePair(-650.5d, BigDecimal.valueOf(-650.5d)), + new TestablePair(18_446_744_073_709_551_616.0f, new BigDecimal("18446744073709551616")), + new TestablePair(18_446_744_073_709_551_616.0d, new BigDecimal("18446744073709551616")), + new TestablePair(new BigInteger("18446744073709551616"), new BigDecimal("18446744073709551616")), + new TestablePair("18446744073709551615", new BigDecimal("18446744073709551615")), + }; + + testValuesForRegistry(converterRegistry) + .fromValues(pairs) + .to(BigDecimal.class) + .assertValues(); + } + + @Test + void testUnsupportedToBigDecimalConversions() { + Object[] unsupportedValues = { + Float.NaN, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, + Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, + "five with half", "NaN", "Infinity", "-Infinity" + }; + + testValuesForRegistry(converterRegistry) + .fromNotConvertibleValues(unsupportedValues) + .to(BigDecimal.class) + .assertNotConvertible(); + } + + @Test + void testIsConvertibleToString() { + testTypesForRegistry(converterRegistry) + .from( + Byte.class, Short.class, Integer.class, Long.class, + Float.class, Double.class, BigInteger.class, BigDecimal.class, + Boolean.class, byte[].class + ) + .to(String.class) + .assertTypes(); + } + + @Test + void testToStringConversions() { + TestablePair[] pairs = { + new TestablePair((byte) 127, "127"), new TestablePair((byte) -128, "-128"), + new TestablePair((short) 32767, "32767"), new TestablePair((short) -32768, "-32768"), + new TestablePair(2147483647, "2147483647"), new TestablePair(-2147483648, "-2147483648"), + new TestablePair(9223372036854775807L, "9223372036854775807"), + new TestablePair(-9223372036854775808L, "-9223372036854775808"), + + new TestablePair(0x1.fffffffffffffP+1023, "1.7976931348623157E308"), + new TestablePair(0x0.0000000000001P-1022, "4.9E-324"), + new TestablePair(0.001, "0.001"), new TestablePair(0.0001, "1.0E-4"), + new TestablePair(0.001, "0.001"), new TestablePair(0.0001, "1.0E-4"), + new TestablePair(Double.NaN, "NaN"), new TestablePair(10_000_000_000_000_000_000.0, "1.0E19"), + new TestablePair(Double.NEGATIVE_INFINITY, "-Infinity"), + new TestablePair(Double.POSITIVE_INFINITY, "Infinity"), + + new TestablePair(BigInteger.valueOf(2).pow(64).subtract(BigInteger.ONE), "18446744073709551615"), + new TestablePair(BigDecimal.valueOf(10_000_000_000_000_000_000.0), "1.0E+19"), + new TestablePair(BigDecimal.valueOf(0x0.0000000000001P-1022), "4.9E-324"), + + new TestablePair(true, "true"), new TestablePair(false, "false"), + new TestablePair( + new byte[] {(byte) 0x67, (byte) 0x61, (byte) 0x72, (byte) 0xc3, (byte) 0xa7, (byte) 0x6f, (byte) 0x6e}, + "garçon"), + }; + + testValuesForRegistry(converterRegistry) + .fromValues(pairs) + .to(String.class) + .assertValues(); + } + + /** + * Tests invalid UTF-8 byte sequences. + * + * @see Bad UTF8 + */ + @Test + void testUnsupportedToStringConversions() { + Object[] unsupportedValues = { + // 0xc0 is not a valid UTF-8 byte + new byte[] { 0x68, (byte) 0xc0, 0x65, 0x6c, 0x6c, 0x6f }, + // an unexpected continuation byte 0xdc (0b11011100) but 0b10xxxxxx is required as the second byte + new byte[] { (byte) 0xc2, (byte) 0xdc } + }; + + testValuesForRegistry(converterRegistry) + .fromNotConvertibleValues(unsupportedValues) + .to(String.class) + .assertNotConvertible(); + } + + @Test + void testIsConvertibleToByteArray() { + testTypesForRegistry(converterRegistry) + .from(String.class) + .notFrom( + Byte.class, Short.class, Integer.class, Long.class, + Float.class, Double.class, BigInteger.class, BigDecimal.class, + Boolean.class + ) + .to(byte[].class) + .assertTypes(); + } + + @Test + void testToByteArrayConversions() { + TestablePair[] pairs = { + new TestablePair( + "hello", + new byte[] { 0x68, 0x65, 0x6c, 0x6c, 0x6f } + ), + new TestablePair( + "привет", + new byte[] { (byte) 0xd0, (byte) 0xbf, (byte) 0xd1, (byte) 0x80, (byte) 0xd0, (byte) 0xb8, (byte) 0xd0, + (byte) 0xb2, (byte) 0xd0, (byte) 0xb5, (byte) 0xd1, (byte) 0x82 } + ), + new TestablePair( + "你好", + new byte[] { (byte) 0xe4, (byte) 0xbd, (byte) 0xa0, (byte) 0xe5, (byte) 0xa5, (byte) 0xbd }), + new TestablePair("hello", new byte[] { 0x68, 0x65, 0x6c, 0x6c, 0x6f }) + }; + + for (TestablePair pair : pairs) { + assertArrayEquals((byte[]) pair.output, converterRegistry.convert(pair.input, byte[].class)); + } + } + + @Test + void testRemoveConvertible() { + assertEquals(10, converterRegistry.convert("10", Integer.class)); + + converterRegistry.removeConvertible(String.class, Integer.class); + NotConvertibleValueException error = assertThrows( + NotConvertibleValueException.class, + () -> converterRegistry.convert("10", Integer.class) + ); + assertEquals(String.class, error.getFrom()); + assertEquals(Integer.class, error.getTo()); + } + + @Test + void testReplaceConvertible() { + assertEquals(200, converterRegistry.convert("200", Integer.class)); + + converterRegistry.addConverter(String.class, Integer.class, String::length); + assertEquals(3, converterRegistry.convert("200", Integer.class)); + } + + private static class TestablePair { + public TestablePair(Object input, Object output) { + this.input = input; + this.output = output; + } + + Object input; + Object output; + } + + static class ConversionValuesTester { + private final ConverterRegistry registry; + private Class to; + private TestablePair[] pairs = new TestablePair[0]; + private Object[] ncvObjects = new Object[0]; + + static ConversionValuesTester testValuesForRegistry(ConverterRegistry registry) { + return new ConversionValuesTester(registry); + } + + ConversionValuesTester(ConverterRegistry registry) { + this.registry = registry; + } + + ConversionValuesTester to(Class to) { + this.to = to; + return this; + } + + ConversionValuesTester fromValues(TestablePair... pairs) { + this.pairs = pairs; + return this; + } + + ConversionValuesTester fromNotConvertibleValues(Object... values) { + this.ncvObjects = values; + return this; + } + + void assertValues() { + for (TestablePair pair : pairs) { + assertEquals(pair.output, registry.convert(pair.input, to)); + } + } + + void assertNotConvertible() { + for (Object object : ncvObjects) { + assertThrows(NotConvertibleValueException.class, () -> registry.convert(object, to)); + } + } + } + + static class ConversionTypesTester { + private final ConverterRegistry registry; + private Class to; + private Class[] convertibleSources = new Class[0]; + private Class[] unconvertibleSources = new Class[0]; + + static ConversionTypesTester testTypesForRegistry(ConverterRegistry registry) { + return new ConversionTypesTester(registry); + } + + ConversionTypesTester(ConverterRegistry registry) { + this.registry = registry; + } + + ConversionTypesTester to(Class to) { + this.to = to; + return this; + } + + ConversionTypesTester from(Class... from) { + this.convertibleSources = from; + return this; + } + + ConversionTypesTester notFrom(Class... from) { + this.unconvertibleSources = from; + return this; + } + + void assertTypes() { + for (Class from : convertibleSources) { + assertTrue(registry.isConvertible(from, to)); + } + for (Class from : unconvertibleSources) { + assertFalse(registry.isConvertible(from, to)); + } + } + } +}