blob: 6e3a487ad53f099eb69578681058049d056dfd43 [file]
/*
* Copyright 2023 CeresDB Project Authors. Licensed under Apache-2.0.
*/
package io.ceresdb;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.ceresdb.models.Row;
import io.ceresdb.proto.internal.Common;
import io.ceresdb.proto.internal.Storage;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
import org.apache.arrow.vector.BigIntVector;
import org.apache.arrow.vector.BitVector;
import org.apache.arrow.vector.FieldVector;
import org.apache.arrow.vector.Float4Vector;
import org.apache.arrow.vector.Float8Vector;
import org.apache.arrow.vector.IntVector;
import org.apache.arrow.vector.SmallIntVector;
import org.apache.arrow.vector.TimeStampMilliVector;
import org.apache.arrow.vector.TinyIntVector;
import org.apache.arrow.vector.UInt1Vector;
import org.apache.arrow.vector.UInt2Vector;
import org.apache.arrow.vector.UInt4Vector;
import org.apache.arrow.vector.UInt8Vector;
import org.apache.arrow.vector.VarBinaryVector;
import org.apache.arrow.vector.VarCharVector;
import org.apache.arrow.vector.VectorSchemaRoot;
import org.apache.arrow.vector.ipc.ArrowStreamWriter;
import org.apache.arrow.vector.types.pojo.Field;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import io.ceresdb.common.Endpoint;
import io.ceresdb.common.util.internal.ThrowUtil;
import io.ceresdb.errors.IteratorException;
import io.ceresdb.errors.StreamException;
import io.ceresdb.models.Err;
import io.ceresdb.models.SqlQueryOk;
import io.ceresdb.models.SqlQueryRequest;
import io.ceresdb.models.Result;
import io.ceresdb.options.QueryOptions;
import io.ceresdb.rpc.Context;
import io.ceresdb.rpc.Observer;
import io.ceresdb.util.Utils;
import com.google.protobuf.ByteStringHelper;
@RunWith(value = MockitoJUnitRunner.class)
public class QueryClientTest {
private QueryClient queryClient;
@Mock
private RouterClient routerClient;
@Before
public void before() {
final QueryOptions queryOpts = new QueryOptions();
queryOpts.setAsyncPool(ForkJoinPool.commonPool());
queryOpts.setRouterClient(this.routerClient);
queryOpts.setDatabase("public");
this.queryClient = new QueryClient();
this.queryClient.init(queryOpts);
}
@After
public void after() {
this.queryClient.shutdownGracefully();
this.routerClient.shutdownGracefully();
}
@Test
public void queryOkNoRouteTest() throws ExecutionException, InterruptedException, IOException {
final Storage.SqlQueryResponse resp = mockSimpleQueryResponse(1, false);
final Endpoint ep = Endpoint.of("127.0.0.1", 8081);
Mockito.when(this.routerClient.invoke(Mockito.eq(ep), Mockito.any(), Mockito.any())) //
.thenReturn(Utils.completedCf(resp));
Mockito.when(this.routerClient.routeFor(Mockito.any(), Mockito.any())) //
.thenReturn(Utils.completedCf(new HashMap<>()));
Mockito.when(this.routerClient.clusterRoute()) //
.thenReturn(Route.of(ep));
final SqlQueryRequest req = SqlQueryRequest.newBuilder().forTables("query_test_table") //
.sql("select number from query_test_table") //
.build();
final CompletableFuture<Result<SqlQueryOk, Err>> f = this.queryClient.sqlQuery(req, Context.newDefault());
final Result<SqlQueryOk, Err> r = f.get();
Assert.assertTrue(r.isOk());
final SqlQueryOk queryOk = r.getOk();
final Stream<String> strs = queryOk.map(Row::toString);
Assert.assertEquals(
Collections.singletonList("t1:Value{type=String,value=tvtest}|f1:Value{type=Int32,value=123}"),
strs.collect(Collectors.toList()));
}
@Test
public void queryOkByValidRouteTest() throws ExecutionException, InterruptedException, IOException {
final Storage.SqlQueryResponse resp = mockSimpleQueryResponse(1, false);
final Endpoint ep = Endpoint.of("127.0.0.1", 8081);
Mockito.when(this.routerClient.invoke(Mockito.eq(ep), Mockito.any(), Mockito.any())) //
.thenReturn(Utils.completedCf(resp));
Mockito.when(
this.routerClient.routeFor(Mockito.any(), Mockito.eq(Collections.singletonList("query_test_table")))) //
.thenReturn(Utils.completedCf(new HashMap<String, Route>() {
private static final long serialVersionUID = -6260265905617276356L;
{
put("query_test_table", Route.of("query_test_table", ep));
}
}));
final SqlQueryRequest req = SqlQueryRequest.newBuilder().forTables("query_test_table") //
.sql("select number from query_test_table") //
.build();
final CompletableFuture<Result<SqlQueryOk, Err>> f = this.queryClient.sqlQuery(req, Context.newDefault());
final Result<SqlQueryOk, Err> r = f.get();
Assert.assertTrue(r.isOk());
final SqlQueryOk queryOk = r.getOk();
final Stream<String> strs = queryOk.map(Row::toString);
Assert.assertEquals(
Collections.singletonList("t1:Value{type=String,value=tvtest}|f1:Value{type=Int32,value=123}"),
strs.collect(Collectors.toList()));
}
@Test
public void queryFailedTest() throws ExecutionException, InterruptedException, IOException {
final Common.ResponseHeader header = Common.ResponseHeader.newBuilder() //
.setCode(500) // failed code
.build();
final Storage.SqlQueryResponse resp = Storage.SqlQueryResponse.newBuilder().setHeader(header) //
.build();
final Endpoint ep = Endpoint.of("127.0.0.1", 8081);
Mockito.when(this.routerClient.invoke(Mockito.eq(ep), Mockito.any(), Mockito.any())) //
.thenReturn(Utils.completedCf(resp));
Mockito.when(
this.routerClient.routeFor(Mockito.any(), Mockito.eq(Collections.singletonList("query_test_table")))) //
.thenReturn(Utils.completedCf(new HashMap<String, Route>() {
private static final long serialVersionUID = 4517371149948738282L;
{
put("query_test_table", Route.of("query_test_table", ep));
}
}));
Mockito.when(this.routerClient.routeRefreshFor(Mockito.any(), Mockito.any()))
.thenReturn(Utils.completedCf(new HashMap<String, Route>() {
private static final long serialVersionUID = 2347114952231996366L;
{
put("query_test_table", Route.of("query_test_table", ep));
}
}));
final SqlQueryRequest req = SqlQueryRequest.newBuilder().forTables("query_test_table") //
.sql("select number from query_test_table") //
.build();
final CompletableFuture<Result<SqlQueryOk, Err>> f = this.queryClient.sqlQuery(req, Context.newDefault());
final Result<SqlQueryOk, Err> r = f.get();
Assert.assertFalse(r.isOk());
final Err err = r.getErr();
Assert.assertEquals(ep, err.getErrTo());
Assert.assertEquals(Collections.singletonList("query_test_table"), err.getFailedTables());
}
@Test
public void queryByArrowFullTypeTest() throws ExecutionException, InterruptedException, IOException {
final Result<SqlQueryOk, Err> r = queryByArrow();
Assert.assertTrue(r.isOk());
final SqlQueryOk queryOk = r.getOk();
queryOk.stream().forEach(this::checkFullTypeRow);
}
private void checkFullTypeRow(final Row row) {
Assert.assertEquals(14, row.getColumnCount());
Assert.assertEquals("test", row.getColumn("fString").getValue().getString());
Assert.assertEquals(Boolean.TRUE, row.getColumn("fBool").getValue().getBoolean());
Assert.assertEquals(0.64, row.getColumn("fDouble").getValue().getDouble(), 0.000001);
Assert.assertEquals(0.32f, row.getColumn("fFloat").getValue().getFloat(), 0.000001);
Assert.assertEquals(-64, row.getColumn("fInt64").getValue().getInt64());
Assert.assertEquals(-32, row.getColumn("fInt32").getValue().getInt32());
Assert.assertEquals(-16, row.getColumn("fInt16").getValue().getInt16());
Assert.assertEquals(-8, row.getColumn("fInt8").getValue().getInt8());
Assert.assertEquals(64, row.getColumn("fUint64").getValue().getUInt64());
Assert.assertEquals(32, row.getColumn("fUint32").getValue().getUInt32());
Assert.assertEquals(16, row.getColumn("fUint16").getValue().getUInt16());
Assert.assertEquals(8, row.getColumn("fUint8").getValue().getUInt8());
Assert.assertEquals(1675345488158L, row.getColumn("fTimestamp").getValue().getTimestamp());
Assert.assertArrayEquals(new byte[] { 1, 2, 3 }, row.getColumn("fVarbinary").getValue().getVarbinary());
}
@Test
public void queryByArrowNullTypeTest() throws ExecutionException, InterruptedException, IOException {
final Storage.SqlQueryResponse resp = mockSimpleQueryResponse(1, true);
final Endpoint ep = Endpoint.of("127.0.0.1", 8081);
Mockito.when(this.routerClient.invoke(Mockito.eq(ep), Mockito.any(), Mockito.any())) //
.thenReturn(Utils.completedCf(resp));
Mockito.when(this.routerClient.routeFor(Mockito.any(), Mockito.any())) //
.thenReturn(Utils.completedCf(new HashMap<>()));
Mockito.when(this.routerClient.clusterRoute()) //
.thenReturn(Route.of(ep));
final SqlQueryRequest req = SqlQueryRequest.newBuilder().forTables("query_test_table") //
.sql("select * from query_test_table") //
.build();
final CompletableFuture<Result<SqlQueryOk, Err>> f = this.queryClient.sqlQuery(req, Context.newDefault());
final Result<SqlQueryOk, Err> r = f.get();
Assert.assertTrue(r.isOk());
final SqlQueryOk queryOk = r.getOk();
Row row = queryOk.stream().findFirst().get();
Assert.assertEquals("tvtest", row.getColumn("t1").getValue().getString());
Assert.assertTrue(row.getColumn("f1").getValue().isNull());
}
private Result<SqlQueryOk, Err> queryByArrow() throws IOException, ExecutionException, InterruptedException {
final Storage.SqlQueryResponse resp = mockAllTypeQueryResponse(1);
final Endpoint ep = Endpoint.of("127.0.0.1", 8081);
Mockito.when(this.routerClient.invoke(Mockito.eq(ep), Mockito.any(), Mockito.any())) //
.thenReturn(Utils.completedCf(resp));
Mockito.when(this.routerClient.routeFor(Mockito.any(), Mockito.any())) //
.thenReturn(Utils.completedCf(new HashMap<>()));
Mockito.when(this.routerClient.clusterRoute()) //
.thenReturn(Route.of(ep));
final SqlQueryRequest req = SqlQueryRequest.newBuilder().forTables("query_test_table") //
.sql("select number from query_test_table") //
.build();
final CompletableFuture<Result<SqlQueryOk, Err>> f = this.queryClient.sqlQuery(req, Context.newDefault());
return f.get();
}
private SqlQueryOk mockQueryOk() {
try {
Thread.sleep(20);
return queryByArrow().unwrapOr(SqlQueryOk.emptyOk());
} catch (final Throwable t) {
ThrowUtil.throwException(t);
}
return null;
}
private Storage.SqlQueryResponse mockSimpleQueryResponse(int rowCount, boolean hasNullField) throws IOException {
final Common.ResponseHeader header = Common.ResponseHeader.newBuilder() //
.setCode(Result.SUCCESS) //
.build();
BufferAllocator allocator = new RootAllocator();
VarCharVector varCharVector = new VarCharVector("t1", allocator);
IntVector intVector = new IntVector("f1", allocator);
for (int i = 0; i < rowCount; i++) {
varCharVector.setSafe(i, "tvtest".getBytes());
if (hasNullField) {
intVector.setNull(i);
} else {
intVector.setSafe(i, 123);
}
}
varCharVector.setValueCount(rowCount);
intVector.setValueCount(rowCount);
List<Field> fields = Arrays.asList(varCharVector.getField(), intVector.getField());
List<FieldVector> vectors = Arrays.asList(varCharVector, intVector);
VectorSchemaRoot root = new VectorSchemaRoot(fields, vectors);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ArrowStreamWriter writer = new ArrowStreamWriter(root, /*DictionaryProvider=*/null, out);
writer.writeBatch();
writer.close();
Storage.ArrowPayload arrowPayload = Storage.ArrowPayload.newBuilder()
.setCompression(Storage.ArrowPayload.Compression.NONE)
.addRecordBatches(ByteStringHelper.wrap(out.toByteArray())).build();
return Storage.SqlQueryResponse.newBuilder() //
.setHeader(header) //
.setArrow(arrowPayload).build();
}
private Storage.SqlQueryResponse mockAllTypeQueryResponse(int rowCount) throws IOException {
final Common.ResponseHeader header = Common.ResponseHeader.newBuilder() //
.setCode(Result.SUCCESS) //
.build();
BufferAllocator allocator = new RootAllocator();
VarCharVector stringVector = new VarCharVector("fString", allocator);
BitVector boolVector = new BitVector("fBool", allocator);
Float8Vector doubleVector = new Float8Vector("fDouble", allocator);
Float4Vector floatVector = new Float4Vector("fFloat", allocator);
BigIntVector int64Vector = new BigIntVector("fInt64", allocator);
IntVector int32Vector = new IntVector("fInt32", allocator);
SmallIntVector int16Vector = new SmallIntVector("fInt16", allocator);
TinyIntVector int8Vector = new TinyIntVector("fInt8", allocator);
UInt8Vector uint64Vector = new UInt8Vector("fUint64", allocator);
UInt4Vector uint32Vector = new UInt4Vector("fUint32", allocator);
UInt2Vector uint16Vector = new UInt2Vector("fUint16", allocator);
UInt1Vector uint8Vector = new UInt1Vector("fUint8", allocator);
TimeStampMilliVector tsVector = new TimeStampMilliVector("fTimestamp", allocator);
VarBinaryVector varBinaryVector = new VarBinaryVector("fVarbinary", allocator);
for (int i = 0; i < rowCount; i++) {
stringVector.setSafe(i, "test".getBytes());
boolVector.setSafe(i, 1);
doubleVector.setSafe(i, 0.64);
floatVector.setSafe(i, 0.32f);
int64Vector.setSafe(i, -64);
int32Vector.setSafe(i, -32);
int16Vector.setSafe(i, -16);
int8Vector.setSafe(i, -8);
uint64Vector.setSafe(i, 64);
uint32Vector.setSafe(i, 32);
uint16Vector.setSafe(i, 16);
uint8Vector.setSafe(i, 8);
tsVector.setSafe(i, 1675345488158L);
varBinaryVector.setSafe(i, new byte[] { 1, 2, 3 });
}
stringVector.setValueCount(rowCount);
boolVector.setValueCount(rowCount);
doubleVector.setValueCount(rowCount);
floatVector.setValueCount(rowCount);
int64Vector.setValueCount(rowCount);
int32Vector.setValueCount(rowCount);
int16Vector.setValueCount(rowCount);
int8Vector.setValueCount(rowCount);
uint64Vector.setValueCount(rowCount);
uint32Vector.setValueCount(rowCount);
uint16Vector.setValueCount(rowCount);
uint8Vector.setValueCount(rowCount);
tsVector.setValueCount(rowCount);
varBinaryVector.setValueCount(rowCount);
List<Field> fields = Arrays.asList(stringVector.getField(), boolVector.getField(), doubleVector.getField(),
floatVector.getField(), int64Vector.getField(), int32Vector.getField(), int16Vector.getField(),
int8Vector.getField(), uint64Vector.getField(), uint32Vector.getField(), uint16Vector.getField(),
uint8Vector.getField(), tsVector.getField(), varBinaryVector.getField());
List<FieldVector> vectors = Arrays.asList(stringVector, boolVector, doubleVector, floatVector, int64Vector,
int32Vector, int16Vector, int8Vector, uint64Vector, uint32Vector, uint16Vector, uint8Vector, tsVector,
varBinaryVector);
VectorSchemaRoot root = new VectorSchemaRoot(fields, vectors);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ArrowStreamWriter writer = new ArrowStreamWriter(root, /*DictionaryProvider=*/null, out);
writer.writeBatch();
writer.close();
Storage.ArrowPayload arrowPayload = Storage.ArrowPayload.newBuilder()
.setCompression(Storage.ArrowPayload.Compression.NONE)
.addRecordBatches(ByteStringHelper.wrap(out.toByteArray())).build();
return Storage.SqlQueryResponse.newBuilder() //
.setHeader(header) //
.setArrow(arrowPayload).build();
}
@Test
public void blockingStreamQueryTest() {
final SqlQueryOk data = mockQueryOk();
final int respCount = 10;
final BlockingStreamIterator streams = new BlockingStreamIterator(1000, TimeUnit.MILLISECONDS);
final Observer<SqlQueryOk> obs = streams.getObserver();
new Thread(() -> {
for (int i = 0; i < respCount; i++) {
if (i == 0) {
obs.onNext(data);
} else {
obs.onNext(mockQueryOk());
}
}
obs.onCompleted();
}).start();
final Iterator<Row> it = new RowIterator(streams);
int i = 0;
while (it.hasNext()) {
i++;
checkFullTypeRow(it.next());
}
Assert.assertEquals(respCount, i);
}
@Test(expected = IteratorException.class)
public void blockingStreamQueryOnErrTest() {
final int respCount = 10;
final BlockingStreamIterator streams = new BlockingStreamIterator(1000, TimeUnit.MILLISECONDS);
final Observer<SqlQueryOk> obs = streams.getObserver();
new Thread(() -> {
for (int i = 0; i < respCount; i++) {
if (i == 5) {
obs.onError(new StreamException("fail to stream query"));
} else {
obs.onNext(mockQueryOk());
}
}
obs.onCompleted();
}).start();
final Iterator<Row> it = new RowIterator(streams);
int i = 0;
while (it.hasNext()) {
i++;
checkFullTypeRow(it.next());
}
Assert.assertEquals(respCount, i);
}
}