| /* |
| * 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); |
| } |
| } |