This is an automated email from the ASF dual-hosted git repository.

jackie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 38acdc6d29 Use string to represent BigDecimal response (#11716)
38acdc6d29 is described below

commit 38acdc6d29229a74235a86ea6b7dd7603ca35bf9
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Mon Oct 2 13:31:45 2023 -0700

    Use string to represent BigDecimal response (#11716)
---
 .../org/apache/pinot/common/utils/DataSchema.java  |   7 +-
 .../apache/pinot/common/utils/DataSchemaTest.java  |  10 +
 .../pinot/queries/BigDecimalQueriesTest.java       |  79 ++---
 .../apache/pinot/queries/DistinctQueriesTest.java  |   6 +-
 .../org/apache/pinot/queries/ExprMinMaxTest.java   |   4 +-
 .../query/runtime/queries/QueryRunnerTest.java     |  18 +-
 .../query/runtime/queries/QueryRunnerTestBase.java | 329 ++++++++++++---------
 .../runtime/queries/ResourceBasedQueriesTest.java  |  29 +-
 8 files changed, 256 insertions(+), 226 deletions(-)

diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/utils/DataSchema.java 
b/pinot-common/src/main/java/org/apache/pinot/common/utils/DataSchema.java
index fabad7a049..e9a7e858fe 100644
--- a/pinot-common/src/main/java/org/apache/pinot/common/utils/DataSchema.java
+++ b/pinot-common/src/main/java/org/apache/pinot/common/utils/DataSchema.java
@@ -449,10 +449,13 @@ public class DataSchema {
     }
 
     /**
-     * Formats the value to human-readable format based on the type to be used 
in the query response.
+     * Formats the value based on the type to be used in the JSON query 
response. For BIG_DECIMAL, even though JSON can
+     * serialize BigDecimal, it is best practice to convert it to String to 
avoid precision loss during deserialization.
      */
     public Serializable format(Object value) {
       switch (this) {
+        case BIG_DECIMAL:
+          return ((BigDecimal) value).toPlainString();
         case TIMESTAMP:
           assert value instanceof Timestamp;
           return value.toString();
@@ -479,7 +482,7 @@ public class DataSchema {
         case DOUBLE:
           return ((Number) value).doubleValue();
         case BIG_DECIMAL:
-          return (BigDecimal) value;
+          return ((BigDecimal) value).toPlainString();
         case BOOLEAN:
           return ((int) value) == 1;
         case TIMESTAMP:
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/utils/DataSchemaTest.java 
b/pinot-common/src/test/java/org/apache/pinot/common/utils/DataSchemaTest.java
index 65eae2ce97..3a22b1d30c 100644
--- 
a/pinot-common/src/test/java/org/apache/pinot/common/utils/DataSchemaTest.java
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/utils/DataSchemaTest.java
@@ -18,8 +18,11 @@
  */
 package org.apache.pinot.common.utils;
 
+import java.math.BigDecimal;
 import java.nio.ByteBuffer;
+import java.sql.Timestamp;
 import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.utils.BytesUtils;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -178,5 +181,12 @@ public class DataSchemaTest {
     Assert.assertEquals(fromDataType(FieldSpec.DataType.BOOLEAN, false), 
BOOLEAN_ARRAY);
     Assert.assertEquals(fromDataType(FieldSpec.DataType.TIMESTAMP, false), 
TIMESTAMP_ARRAY);
     Assert.assertEquals(fromDataType(FieldSpec.DataType.BYTES, false), 
BYTES_ARRAY);
+
+    BigDecimal bigDecimalValue = new 
BigDecimal("1.2345678901234567890123456789");
+    Assert.assertEquals(BIG_DECIMAL.format(bigDecimalValue), 
bigDecimalValue.toPlainString());
+    Timestamp timestampValue = new Timestamp(1234567890123L);
+    Assert.assertEquals(TIMESTAMP.format(timestampValue), 
timestampValue.toString());
+    byte[] bytesValue = {12, 34, 56};
+    Assert.assertEquals(BYTES.format(bytesValue), 
BytesUtils.toHexString(bytesValue));
   }
 }
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/queries/BigDecimalQueriesTest.java 
b/pinot-core/src/test/java/org/apache/pinot/queries/BigDecimalQueriesTest.java
index 9e37d669bb..3d50c89484 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/queries/BigDecimalQueriesTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/queries/BigDecimalQueriesTest.java
@@ -138,9 +138,7 @@ public class BigDecimalQueriesTest extends BaseQueriesTest {
   @Test
   public void testQueriesWithDictColumn()
       throws Exception {
-    TableConfig tableConfig = new TableConfigBuilder(TableType.OFFLINE)
-        .setTableName(RAW_TABLE_NAME)
-        .build();
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(RAW_TABLE_NAME).build();
     setUp(tableConfig);
     testQueries();
   }
@@ -150,10 +148,8 @@ public class BigDecimalQueriesTest extends BaseQueriesTest 
{
       throws Exception {
     List<String> noDictionaryColumns = new ArrayList<String>();
     noDictionaryColumns.add(BIG_DECIMAL_COLUMN);
-    TableConfig tableConfig = new TableConfigBuilder(TableType.OFFLINE)
-        .setTableName(RAW_TABLE_NAME)
-        .setNoDictionaryColumns(noDictionaryColumns)
-        .build();
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(RAW_TABLE_NAME)
+        .setNoDictionaryColumns(noDictionaryColumns).build();
     setUp(tableConfig);
     testQueries();
   }
@@ -174,7 +170,7 @@ public class BigDecimalQueriesTest extends BaseQueriesTest {
         Object[] row = rows.get(i);
         assertEquals(row.length, 1);
         if (row[0] != null) {
-          assertEquals(row[0], BASE_BIG_DECIMAL.add(BigDecimal.valueOf(i)));
+          assertEquals(row[0], 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(i)).toPlainString());
         }
       }
     }
@@ -204,7 +200,7 @@ public class BigDecimalQueriesTest extends BaseQueriesTest {
           if (k >= NUM_RECORDS) {
             assertNull(values[0]);
           } else {
-            assertEquals(values[0], 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(NUM_RECORDS - 1 - k)));
+            assertEquals(values[0], 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(NUM_RECORDS - 1 - k)).toPlainString());
           }
         }
         k++;
@@ -228,7 +224,7 @@ public class BigDecimalQueriesTest extends BaseQueriesTest {
         if (i % 4 == 3) {
           i++;
         }
-        assertEquals(row[0], BASE_BIG_DECIMAL.add(BigDecimal.valueOf(i)));
+        assertEquals(row[0], 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(i)).toPlainString());
         i++;
       }
       // The default null ordering is 'NULLS LAST'. Therefore, null will 
appear as the last record.
@@ -236,8 +232,8 @@ public class BigDecimalQueriesTest extends BaseQueriesTest {
     }
     {
       int limit = 40;
-      String query = String.format("SELECT DISTINCT %s FROM testTable ORDER BY 
%s LIMIT %d",
-          BIG_DECIMAL_COLUMN, BIG_DECIMAL_COLUMN, limit);
+      String query = String.format("SELECT DISTINCT %s FROM testTable ORDER BY 
%s LIMIT %d", BIG_DECIMAL_COLUMN,
+          BIG_DECIMAL_COLUMN, limit);
       BrokerResponseNative brokerResponse = getBrokerResponse(query, 
queryOptions);
       ResultTable resultTable = brokerResponse.getResultTable();
       DataSchema dataSchema = resultTable.getDataSchema();
@@ -255,7 +251,7 @@ public class BigDecimalQueriesTest extends BaseQueriesTest {
         if (i % 4 == 3) {
           i++;
         }
-        assertEquals(row[0], BASE_BIG_DECIMAL.add(BigDecimal.valueOf(i)));
+        assertEquals(row[0], 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(i)).toPlainString());
         i++;
         index++;
       }
@@ -285,13 +281,14 @@ public class BigDecimalQueriesTest extends 
BaseQueriesTest {
       assertEquals((long) rows.get(0)[0], 3 * NUM_RECORDS);
     }
     {
-      String query = String.format("SELECT %s FROM testTable GROUP BY %s ORDER 
BY %s DESC",
-          BIG_DECIMAL_COLUMN, BIG_DECIMAL_COLUMN, BIG_DECIMAL_COLUMN);
+      String query =
+          String.format("SELECT %s FROM testTable GROUP BY %s ORDER BY %s 
DESC", BIG_DECIMAL_COLUMN, BIG_DECIMAL_COLUMN,
+              BIG_DECIMAL_COLUMN);
       BrokerResponseNative brokerResponse = getBrokerResponse(query, 
queryOptions);
       ResultTable resultTable = brokerResponse.getResultTable();
       DataSchema dataSchema = resultTable.getDataSchema();
-      assertEquals(dataSchema, new DataSchema(new String[]{BIG_DECIMAL_COLUMN},
-          new ColumnDataType[]{ColumnDataType.BIG_DECIMAL}));
+      assertEquals(dataSchema,
+          new DataSchema(new String[]{BIG_DECIMAL_COLUMN}, new 
ColumnDataType[]{ColumnDataType.BIG_DECIMAL}));
       List<Object[]> rows = resultTable.getRows();
       assertEquals(rows.size(), 10);
       // The default null ordering is 'NULLS LAST'. Therefore, null will 
appear as the last record.
@@ -304,7 +301,7 @@ public class BigDecimalQueriesTest extends BaseQueriesTest {
         }
         Object[] row = rows.get(index);
         assertEquals(row.length, 1);
-        assertEquals(row[0], 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(NUM_RECORDS - i - 1)));
+        assertEquals(row[0], 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(NUM_RECORDS - i - 1)).toPlainString());
         index++;
         i++;
       }
@@ -329,7 +326,7 @@ public class BigDecimalQueriesTest extends BaseQueriesTest {
           // Null values are inserted at: index % 4 == 3. All null values are 
grouped into a single null.
           i++;
         }
-        assertEquals(row[1], 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(NUM_RECORDS - i - 1)));
+        assertEquals(row[1], 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(NUM_RECORDS - i - 1)).toPlainString());
         i++;
       }
     }
@@ -346,8 +343,9 @@ public class BigDecimalQueriesTest extends BaseQueriesTest {
     {
       // Note: defining decimal literals within quotes preserves precision.
       BigDecimal lowerLimit = BASE_BIG_DECIMAL.add(BigDecimal.valueOf(69));
-      String query = String.format("SELECT %s FROM testTable WHERE %s > '%s' 
LIMIT 30",
-          BIG_DECIMAL_COLUMN, BIG_DECIMAL_COLUMN, lowerLimit);
+      String query =
+          String.format("SELECT %s FROM testTable WHERE %s > '%s' LIMIT 30", 
BIG_DECIMAL_COLUMN, BIG_DECIMAL_COLUMN,
+              lowerLimit);
       BrokerResponseNative brokerResponse = getBrokerResponse(query, 
queryOptions);
       ResultTable resultTable = brokerResponse.getResultTable();
       DataSchema dataSchema = resultTable.getDataSchema();
@@ -363,14 +361,14 @@ public class BigDecimalQueriesTest extends 
BaseQueriesTest {
           // Null values are inserted at: index % 4 == 3.
           i++;
         }
-        assertEquals(row[0], BASE_BIG_DECIMAL.add(BigDecimal.valueOf(69 + i + 
1)));
+        assertEquals(row[0], BASE_BIG_DECIMAL.add(BigDecimal.valueOf(69 + i + 
1)).toPlainString());
         i++;
       }
     }
     {
       // Note: defining decimal literals within quotes preserves precision.
-      String query = String.format("SELECT %s FROM testTable WHERE %s = '%s'",
-          BIG_DECIMAL_COLUMN, BIG_DECIMAL_COLUMN, 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(69)));
+      String query = String.format("SELECT %s FROM testTable WHERE %s = '%s'", 
BIG_DECIMAL_COLUMN, BIG_DECIMAL_COLUMN,
+          BASE_BIG_DECIMAL.add(BigDecimal.valueOf(69)));
       BrokerResponseNative brokerResponse = getBrokerResponse(query, 
queryOptions);
       ResultTable resultTable = brokerResponse.getResultTable();
       DataSchema dataSchema = resultTable.getDataSchema();
@@ -381,31 +379,17 @@ public class BigDecimalQueriesTest extends 
BaseQueriesTest {
       for (int i = 0; i < 4; i++) {
         Object[] row = rows.get(i);
         assertEquals(row.length, 1);
-        assertEquals(row[0], BASE_BIG_DECIMAL.add(BigDecimal.valueOf(69)));
+        assertEquals(row[0], 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(69)).toPlainString());
       }
     }
     {
-      // This returns currently 25 rows instead of a single row!
-//      int limit = 25;
-//      String query = String.format(
-//          "SELECT SUMPRECISION(%s) AS sum FROM (SELECT %s FROM testTable 
ORDER BY %s LIMIT %d)",
-//          BIG_DECIMAL_COLUMN, BIG_DECIMAL_COLUMN, BIG_DECIMAL_COLUMN, limit);
-//      BrokerResponseNative brokerResponse = getBrokerResponse(query);
-//      ResultTable resultTable = brokerResponse.getResultTable();
-//      DataSchema dataSchema = resultTable.getDataSchema();
-//      assertEquals(dataSchema, new DataSchema(new String[]{"sum"}, new 
ColumnDataType[]{ColumnDataType.BIG_DECIMAL}));
-//      List<Object[]> rows = resultTable.getRows();
-//      assertEquals(rows.size(), 1);
-    }
-    {
-      String query = String.format(
-          "SELECT MAX(%s) AS maxValue FROM testTable GROUP BY %s HAVING 
maxValue < %s ORDER BY maxValue",
-          BIG_DECIMAL_COLUMN, BIG_DECIMAL_COLUMN, 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(5)));
+      String query =
+          String.format("SELECT MAX(%s) AS maxValue FROM testTable GROUP BY %s 
HAVING maxValue < %s ORDER BY maxValue",
+              BIG_DECIMAL_COLUMN, BIG_DECIMAL_COLUMN, 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(5)));
       BrokerResponseNative brokerResponse = getBrokerResponse(query, 
queryOptions);
       ResultTable resultTable = brokerResponse.getResultTable();
       DataSchema dataSchema = resultTable.getDataSchema();
-      assertEquals(dataSchema,
-          new DataSchema(new String[]{"maxValue"}, new 
ColumnDataType[]{ColumnDataType.DOUBLE}));
+      assertEquals(dataSchema, new DataSchema(new String[]{"maxValue"}, new 
ColumnDataType[]{ColumnDataType.DOUBLE}));
       List<Object[]> rows = resultTable.getRows();
       // The default null ordering is: 'NULLS LAST'. This is why the number of 
returned value is 4 and not 5.
       assertEquals(rows.size(), 4);
@@ -423,14 +407,13 @@ public class BigDecimalQueriesTest extends 
BaseQueriesTest {
     }
     {
       int lowerLimit = 991;
-      String query = String.format(
-          "SELECT MAX(%s) AS maxValue FROM testTable GROUP BY %s HAVING 
maxValue > %s ORDER BY maxValue",
-          BIG_DECIMAL_COLUMN, BIG_DECIMAL_COLUMN, 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(lowerLimit)));
+      String query =
+          String.format("SELECT MAX(%s) AS maxValue FROM testTable GROUP BY %s 
HAVING maxValue > %s ORDER BY maxValue",
+              BIG_DECIMAL_COLUMN, BIG_DECIMAL_COLUMN, 
BASE_BIG_DECIMAL.add(BigDecimal.valueOf(lowerLimit)));
       BrokerResponseNative brokerResponse = getBrokerResponse(query, 
queryOptions);
       ResultTable resultTable = brokerResponse.getResultTable();
       DataSchema dataSchema = resultTable.getDataSchema();
-      assertEquals(dataSchema,
-          new DataSchema(new String[]{"maxValue"}, new 
ColumnDataType[]{ColumnDataType.DOUBLE}));
+      assertEquals(dataSchema, new DataSchema(new String[]{"maxValue"}, new 
ColumnDataType[]{ColumnDataType.DOUBLE}));
       List<Object[]> rows = resultTable.getRows();
       assertEquals(rows.size(), 6);
       int i = lowerLimit;
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/queries/DistinctQueriesTest.java 
b/pinot-core/src/test/java/org/apache/pinot/queries/DistinctQueriesTest.java
index 0d01502b21..0f8da45821 100644
--- a/pinot-core/src/test/java/org/apache/pinot/queries/DistinctQueriesTest.java
+++ b/pinot-core/src/test/java/org/apache/pinot/queries/DistinctQueriesTest.java
@@ -1251,7 +1251,7 @@ public class DistinctQueriesTest extends BaseQueriesTest {
         assertEquals(((Long) row[1]).intValue(), intValue);
         assertEquals(((Float) row[2]).intValue(), intValue);
         assertEquals(((Double) row[3]).intValue(), intValue);
-        assertEquals(((BigDecimal) row[4]).intValue(), intValue);
+        assertEquals(Integer.parseInt((String) row[4]), intValue);
         assertEquals(Integer.parseInt((String) row[5]), intValue);
         assertEquals(new String(BytesUtils.toBytes((String) row[6]), 
UTF_8).trim(), row[5]);
         actualValues.add(intValue);
@@ -1319,7 +1319,7 @@ public class DistinctQueriesTest extends BaseQueriesTest {
       for (Object[] row : rows) {
         int intValue = ((Long) row[0]).intValue();
         List<Integer> actualValueList =
-            Arrays.asList(intValue, ((BigDecimal) row[1]).intValue(), ((Float) 
row[2]).intValue(),
+            Arrays.asList(intValue, Integer.parseInt((String) row[1]), 
((Float) row[2]).intValue(),
                 Integer.parseInt((String) row[3]));
         assertEquals((int) actualValueList.get(1), intValue);
         List<Integer> expectedMVValues = new ArrayList<>(2);
@@ -1522,7 +1522,7 @@ public class DistinctQueriesTest extends BaseQueriesTest {
       for (Object[] row : rows) {
         int intValue = ((Long) row[0]).intValue();
         List<Integer> actualValueList =
-            Arrays.asList(intValue, ((BigDecimal) row[1]).intValue(), ((Float) 
row[2]).intValue(),
+            Arrays.asList(intValue, Integer.parseInt((String) row[1]), 
((Float) row[2]).intValue(),
                 Integer.parseInt((String) row[3]));
         assertEquals((int) actualValueList.get(1), intValue);
         List<Integer> expectedMVValues = new ArrayList<>(2);
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/queries/ExprMinMaxTest.java 
b/pinot-core/src/test/java/org/apache/pinot/queries/ExprMinMaxTest.java
index 413685dbe8..564574b96d 100644
--- a/pinot-core/src/test/java/org/apache/pinot/queries/ExprMinMaxTest.java
+++ b/pinot-core/src/test/java/org/apache/pinot/queries/ExprMinMaxTest.java
@@ -257,8 +257,8 @@ public class ExprMinMaxTest extends BaseQueriesTest {
     assertEquals(rows.get(1)[4], new String[]{"a0", "a01", "a02"});
     assertEquals(rows.get(0)[5], 0);
     assertEquals(rows.get(1)[5], 0);
-    assertEquals(rows.get(0)[6], new BigDecimal(360000));
-    assertEquals(rows.get(1)[6], new BigDecimal(360000));
+    assertEquals(rows.get(0)[6], "360000");
+    assertEquals(rows.get(1)[6], "360000");
     assertEquals(rows.get(0)[7], 600D);
     assertEquals(rows.get(1)[7], 600D);
     assertEquals(rows.get(0)[8], 1683138373879L - 1999L);
diff --git 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTest.java
 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTest.java
index d54eb5b1d0..eb11087c2b 100644
--- 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTest.java
+++ 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTest.java
@@ -24,7 +24,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
+import org.apache.pinot.common.response.broker.ResultTable;
 import org.apache.pinot.core.common.datatable.DataTableBuilderFactory;
 import org.apache.pinot.query.QueryEnvironmentTestBase;
 import org.apache.pinot.query.QueryServerEnclosure;
@@ -38,6 +38,7 @@ import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.data.readers.GenericRow;
 import org.apache.pinot.spi.env.PinotConfiguration;
 import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.spi.utils.JsonUtils;
 import org.apache.pinot.spi.utils.builder.TableNameBuilder;
 import org.testng.Assert;
 import org.testng.annotations.AfterClass;
@@ -150,8 +151,8 @@ public class QueryRunnerTest extends QueryRunnerTestBase {
    */
   @Test(dataProvider = "testDataWithSqlToFinalRowCount")
   public void testSqlWithFinalRowCountChecker(String sql, int expectedRows) {
-    List<Object[]> resultRows = queryRunner(sql, null);
-    Assert.assertEquals(resultRows.size(), expectedRows);
+    ResultTable resultTable = queryRunner(sql, null);
+    Assert.assertEquals(resultTable.getRows().size(), expectedRows);
   }
 
   /**
@@ -163,10 +164,10 @@ public class QueryRunnerTest extends QueryRunnerTestBase {
   @Test(dataProvider = "testSql")
   public void testSqlWithH2Checker(String sql)
       throws Exception {
-    List<Object[]> resultRows = queryRunner(sql, null);
+    ResultTable resultTable = queryRunner(sql, null);
     // query H2 for data
     List<Object[]> expectedRows = queryH2(sql);
-    compareRowEquals(resultRows, expectedRows);
+    compareRowEquals(resultTable, expectedRows);
   }
 
   /**
@@ -176,10 +177,9 @@ public class QueryRunnerTest extends QueryRunnerTestBase {
   public void testSqlWithExceptionMsgChecker(String sql, String exceptionMsg) {
     try {
       // query pinot
-      List<Object[]> resultRows = queryRunner(sql, null);
-      Assert.fail(
-          "Expected error with message '" + exceptionMsg + "'. But instead 
rows were returned: " + resultRows.stream()
-              .map(Arrays::toString).collect(Collectors.joining(",\n")));
+      ResultTable resultTable = queryRunner(sql, null);
+      Assert.fail("Expected error with message '" + exceptionMsg + "'. But 
instead rows were returned: "
+          + JsonUtils.objectToPrettyString(resultTable));
     } catch (Exception e) {
       // NOTE: The actual message is (usually) something like:
       //   Received error query execution result block: 
{200=QueryExecutionError:
diff --git 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTestBase.java
 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTestBase.java
index 0c422f3962..b9ee6abfbb 100644
--- 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTestBase.java
+++ 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTestBase.java
@@ -40,10 +40,13 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import org.apache.commons.codec.DecoderException;
 import org.apache.commons.codec.binary.Hex;
 import org.apache.pinot.common.exception.QueryException;
 import org.apache.pinot.common.response.broker.ResultTable;
+import org.apache.pinot.common.utils.DataSchema;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
 import org.apache.pinot.common.utils.config.QueryOptionsUtils;
 import org.apache.pinot.core.query.reduce.ExecutionStatsAggregator;
 import org.apache.pinot.query.QueryEnvironment;
@@ -60,14 +63,17 @@ import 
org.apache.pinot.query.service.dispatch.QueryDispatcher;
 import org.apache.pinot.spi.data.FieldSpec;
 import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.data.readers.GenericRow;
-import org.apache.pinot.spi.utils.ByteArray;
 import org.apache.pinot.spi.utils.BytesUtils;
 import org.apache.pinot.spi.utils.CommonConstants;
 import org.apache.pinot.spi.utils.StringUtil;
 import org.apache.pinot.sql.parsers.CalciteSqlParser;
 import org.apache.pinot.sql.parsers.SqlNodeAndOptions;
 import org.h2.jdbc.JdbcArray;
-import org.testng.Assert;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
 
 
 public abstract class QueryRunnerTestBase extends QueryTestSet {
@@ -95,7 +101,7 @@ public abstract class QueryRunnerTestBase extends 
QueryTestSet {
    * Dispatch query to each pinot-server. The logic should mimic 
QueryDispatcher.submit() but does not actually make
    * ser/de dispatches.
    */
-  protected List<Object[]> queryRunner(String sql, Map<Integer, 
ExecutionStatsAggregator> executionStatsAggregatorMap) {
+  protected ResultTable queryRunner(String sql, Map<Integer, 
ExecutionStatsAggregator> executionStatsAggregatorMap) {
     long requestId = REQUEST_ID_GEN.getAndIncrement();
     SqlNodeAndOptions sqlNodeAndOptions = 
CalciteSqlParser.compileToSqlNodeAndOptions(sql);
     QueryEnvironment.QueryPlannerResult queryPlannerResult =
@@ -140,10 +146,8 @@ public abstract class QueryRunnerTestBase extends 
QueryTestSet {
       }
     }
     // exception will be propagated through for assert purpose on runtime error
-    ResultTable resultTable =
-        QueryDispatcher.runReducer(requestId, dispatchableSubPlan, timeoutMs, 
Collections.emptyMap(),
-            executionStatsAggregatorMap, _mailboxService);
-    return resultTable.getRows();
+    return QueryDispatcher.runReducer(requestId, dispatchableSubPlan, 
timeoutMs, Collections.emptyMap(),
+        executionStatsAggregatorMap, _mailboxService);
   }
 
   protected List<CompletableFuture<?>> 
processDistributedStagePlans(DispatchableSubPlan dispatchableSubPlan,
@@ -193,167 +197,198 @@ public abstract class QueryRunnerTestBase extends 
QueryTestSet {
     return result;
   }
 
-  protected void compareRowEquals(List<Object[]> resultRows, List<Object[]> 
expectedRows) {
-    compareRowEquals(resultRows, expectedRows, false);
+  protected void compareRowEquals(ResultTable resultTable, List<Object[]> 
expectedRows) {
+    compareRowEquals(resultTable, expectedRows, false);
   }
 
-  protected void compareRowEquals(List<Object[]> resultRows, List<Object[]> 
expectedRows,
-      boolean keepOutputRowsInOrder) {
-    Assert.assertEquals(resultRows.size(), expectedRows.size(),
-        String.format("Mismatched number of results. expected: %s, actual: %s",
-            
expectedRows.stream().map(Arrays::toString).collect(Collectors.joining(",\n")),
-            
resultRows.stream().map(Arrays::toString).collect(Collectors.joining(",\n"))));
+  protected void compareRowEquals(ResultTable resultTable, List<Object[]> 
expectedRows, boolean keepOutputRowsInOrder) {
+    List<Object[]> resultRows = resultTable.getRows();
+    int numRows = resultRows.size();
+    assertEquals(numRows, expectedRows.size(), String.format("Mismatched 
number of results. expected: %s, actual: %s",
+        
expectedRows.stream().map(Arrays::toString).collect(Collectors.joining(",\n")),
+        
resultRows.stream().map(Arrays::toString).collect(Collectors.joining(",\n"))));
 
-    Comparator<Object> valueComp = (l, r) -> {
-      if (l == null && r == null) {
-        return 0;
-      } else if (l == null) {
-        return -1;
-      } else if (r == null) {
-        return 1;
+    DataSchema dataSchema = resultTable.getDataSchema();
+    resultRows.forEach(row -> canonicalizeRow(dataSchema, row));
+    expectedRows.forEach(row -> canonicalizeRow(dataSchema, row));
+    if (!keepOutputRowsInOrder) {
+      sortRows(resultRows);
+      sortRows(expectedRows);
+    }
+    for (int i = 0; i < numRows; i++) {
+      Object[] resultRow = resultRows.get(i);
+      Object[] expectedRow = expectedRows.get(i);
+      assertEquals(resultRow.length, expectedRow.length,
+          String.format("Unexpected row size mismatch. Expected: %s, Actual: 
%s", Arrays.toString(expectedRow),
+              Arrays.toString(resultRow)));
+      for (int j = 0; j < resultRow.length; j++) {
+        assertTrue(typeCompatibleFuzzyEquals(dataSchema.getColumnDataType(j), 
resultRow[j], expectedRow[j]),
+            "Not match at (" + i + "," + j + ")! Expected: " + 
Arrays.toString(expectedRow) + " Actual: "
+                + Arrays.toString(resultRow));
       }
-      if (l instanceof Integer) {
-        return Integer.compare((Integer) l, ((Number) r).intValue());
-      } else if (l instanceof Long) {
-        return Long.compare((Long) l, ((Number) r).longValue());
-      } else if (l instanceof Float) {
-        float lf = (Float) l;
-        float rf = ((Number) r).floatValue();
-        if (DoubleMath.fuzzyEquals(lf, rf, DOUBLE_CMP_EPSILON)) {
-          return 0;
-        }
-        float maxf = Math.max(Math.abs(lf), Math.abs(rf));
-        if (DoubleMath.fuzzyEquals(lf / maxf, rf / maxf, DOUBLE_CMP_EPSILON)) {
-          return 0;
-        }
-        return Float.compare(lf, rf);
-      } else if (l instanceof Double) {
-        double ld = (Double) l;
-        double rd = ((Number) r).doubleValue();
-        if (DoubleMath.fuzzyEquals(ld, rd, DOUBLE_CMP_EPSILON)) {
-          return 0;
-        }
-        double maxd = Math.max(Math.abs(ld), Math.abs(rd));
-        if (DoubleMath.fuzzyEquals(ld / maxd, rd / maxd, DOUBLE_CMP_EPSILON)) {
-          return 0;
-        }
-        return Double.compare(ld, rd);
-      } else if (l instanceof String) {
-        if (r instanceof byte[]) {
-          return ((String) l).compareTo(BytesUtils.toHexString((byte[]) r));
-        } else if (r instanceof Timestamp) {
-          return ((String) l).compareTo((r).toString());
-        }
-        return ((String) l).compareTo((String) r);
-      } else if (l instanceof Boolean) {
-        return ((Boolean) l).compareTo((Boolean) r);
-      } else if (l instanceof BigDecimal) {
-        if (r instanceof BigDecimal) {
-          return ((BigDecimal) l).compareTo((BigDecimal) r);
-        } else {
-          return ((BigDecimal) l).compareTo(new BigDecimal((String) r));
+    }
+  }
+
+  protected static void canonicalizeRow(DataSchema dataSchema, Object[] row) {
+    for (int i = 0; i < row.length; i++) {
+      row[i] = canonicalizeValue(dataSchema.getColumnDataType(i), row[i]);
+    }
+  }
+
+  protected static Object canonicalizeValue(ColumnDataType columnDataType, 
Object value) {
+    if (value == null) {
+      return null;
+    }
+    switch (columnDataType) {
+      case INT:
+        return ((Number) value).intValue();
+      case LONG:
+        return ((Number) value).longValue();
+      case FLOAT:
+        return ((Number) value).floatValue();
+      case DOUBLE:
+        return ((Number) value).doubleValue();
+      case BIG_DECIMAL:
+        if (value instanceof String) {
+          return new BigDecimal((String) value);
         }
-      } else if (l instanceof byte[]) {
-        if (r instanceof byte[]) {
-          return ByteArray.compare((byte[]) l, (byte[]) r);
-        } else {
-          return ByteArray.compare((byte[]) l, ((ByteArray) r).getBytes());
+        assertTrue(value instanceof BigDecimal, "Got unexpected value type: " 
+ value.getClass()
+            + " for BIG_DECIMAL column, expected: String or BigDecimal");
+        return value;
+      case BOOLEAN:
+        assertTrue(value instanceof Boolean,
+            "Got unexpected value type: " + value.getClass() + " for BOOLEAN 
column, expected: Boolean");
+        return value;
+      case TIMESTAMP:
+        if (value instanceof String) {
+          return Timestamp.valueOf((String) value);
         }
-      } else if (l instanceof ByteArray) {
-        if (r instanceof ByteArray) {
-          return ((ByteArray) l).compareTo((ByteArray) r);
-        } else {
-          return ByteArray.compare(((ByteArray) l).getBytes(), (byte[]) r);
+        assertTrue(value instanceof Timestamp,
+            "Got unexpected value type: " + value.getClass() + " for TIMESTAMP 
column, expected: String or Timestamp");
+        return value;
+      case STRING:
+        assertTrue(value instanceof String,
+            "Got unexpected value type: " + value.getClass() + " for STRING 
column, expected: String");
+        return value;
+      case BYTES:
+        if (value instanceof byte[]) {
+          return BytesUtils.toHexString((byte[]) value);
         }
-      } else if (l instanceof Timestamp) {
-        return ((Timestamp) l).compareTo((Timestamp) r);
-      } else if (l instanceof int[]) {
-        int[] larray = (int[]) l;
-        try {
-          if (r instanceof JdbcArray) {
-            Object[] rarray = (Object[]) ((JdbcArray) r).getArray();
-            for (int idx = 0; idx < larray.length; idx++) {
-              Number relement = (Number) rarray[idx];
-              if (larray[idx] != relement.intValue()) {
-                return -1;
-              }
-            }
-          } else {
-            int[] rarray = (int[]) r;
-            for (int idx = 0; idx < larray.length; idx++) {
-              if (larray[idx] != rarray[idx]) {
-                return -1;
-              }
+        assertTrue(value instanceof String,
+            "Got unexpected value type: " + value.getClass() + " for BYTES 
column, expected: String or byte[]");
+        return value;
+      case INT_ARRAY:
+        if (value instanceof JdbcArray) {
+          try {
+            Object[] array = (Object[]) ((JdbcArray) value).getArray();
+            int[] intArray = new int[array.length];
+            for (int i = 0; i < array.length; i++) {
+              intArray[i] = ((Number) array[i]).intValue();
             }
+            return intArray;
+          } catch (SQLException e) {
+            throw new RuntimeException(e);
           }
-        } catch (SQLException e) {
-          throw new RuntimeException(e);
         }
-        return 0;
-      } else if (l instanceof String[]) {
-        String[] larray = (String[]) l;
-        try {
-          if (r instanceof JdbcArray) {
-            Object[] rarray = (Object[]) ((JdbcArray) r).getArray();
-            for (int idx = 0; idx < larray.length; idx++) {
-              if (!larray[idx].equals(rarray[idx])) {
-                return -1;
-              }
-            }
-          } else {
-            String[] rarray = (r instanceof List) ? ((List<String>) 
r).toArray(new String[0]) : (String[]) r;
-            for (int idx = 0; idx < larray.length; idx++) {
-              if (!larray[idx].equals(rarray[idx])) {
-                return -1;
-              }
-            }
-          }
-        } catch (SQLException e) {
-          throw new RuntimeException(e);
+        assertTrue(value instanceof int[],
+            "Got unexpected value type: " + value.getClass() + " for INT_ARRAY 
column, expected: int[] or JdbcArray");
+        return value;
+      case STRING_ARRAY:
+        if (value instanceof List) {
+          return ((List) value).toArray(new String[0]);
         }
-        return 0;
-      } else if (l instanceof JdbcArray) {
-        try {
-          Object[] larray = (Object[]) ((JdbcArray) l).getArray();
-          Object[] rarray = (Object[]) ((JdbcArray) r).getArray();
-          for (int idx = 0; idx < larray.length; idx++) {
-            if (!larray[idx].equals(rarray[idx])) {
-              return -1;
+        if (value instanceof JdbcArray) {
+          try {
+            Object[] array = (Object[]) ((JdbcArray) value).getArray();
+            String[] stringArray = new String[array.length];
+            for (int i = 0; i < array.length; i++) {
+              stringArray[i] = (String) array[i];
             }
+            return stringArray;
+          } catch (SQLException e) {
+            throw new RuntimeException(e);
           }
-        } catch (SQLException e) {
-          throw new RuntimeException(e);
         }
+        assertTrue(value instanceof String[], "Got unexpected value type: " + 
value.getClass()
+            + " for STRING_ARRAY column, expected: String[], List or 
JdbcArray");
+        return value;
+      default:
+        throw new UnsupportedOperationException("Unsupported ColumnDataType: " 
+ columnDataType);
+    }
+  }
+
+  protected static void sortRows(List<Object[]> rows) {
+    Comparator<Object> valueComparator = (v1, v2) -> {
+      if (v1 == null && v2 == null) {
         return 0;
-      } else {
-        throw new RuntimeException("non supported type " + l.getClass());
+      } else if (v1 == null) {
+        return -1;
+      } else if (v2 == null) {
+        return 1;
       }
+      if (v1 instanceof Comparable) {
+        return ((Comparable) v1).compareTo(v2);
+      }
+      if (v1 instanceof int[]) {
+        return Arrays.compare((int[]) v1, (int[]) v2);
+      }
+      if (v1 instanceof String[]) {
+        return Arrays.compare((String[]) v1, (String[]) v2);
+      }
+      throw new UnsupportedOperationException("Unsupported class: " + 
v1.getClass());
     };
-    Comparator<Object[]> rowComp = (l, r) -> {
-      int cmp = 0;
-      for (int i = 0; i < l.length; i++) {
-        cmp = valueComp.compare(l[i], r[i]);
+    rows.sort((r1, r2) -> {
+      for (int i = 0; i < r1.length; i++) {
+        int cmp = valueComparator.compare(r1[i], r2[i]);
         if (cmp != 0) {
           return cmp;
         }
       }
       return 0;
-    };
-    if (!keepOutputRowsInOrder) {
-      resultRows.sort(rowComp);
-      expectedRows.sort(rowComp);
+    });
+  }
+
+  protected static boolean typeCompatibleFuzzyEquals(ColumnDataType 
columnDataType, @Nullable Object actual,
+      @Nullable Object expected) {
+    if (actual == null || expected == null) {
+      return actual == expected;
     }
-    for (int i = 0; i < resultRows.size(); i++) {
-      Object[] resultRow = resultRows.get(i);
-      Object[] expectedRow = expectedRows.get(i);
-      Assert.assertEquals(expectedRow.length, resultRow.length,
-          String.format("Unexpected row size mismatch. Expected: %s, Actual: 
%s", Arrays.toString(expectedRow),
-              Arrays.toString(resultRow)));
-      for (int j = 0; j < resultRow.length; j++) {
-        Assert.assertEquals(valueComp.compare(resultRow[j], expectedRow[j]), 0,
-            "Not match at (" + i + "," + j + ")! Expected: " + 
Arrays.toString(expectedRow) + " Actual: "
-                + Arrays.toString(resultRow));
-      }
+
+    switch (columnDataType) {
+      case INT:
+        return (int) actual == ((Number) expected).intValue();
+      case LONG:
+        return (long) actual == ((Number) expected).longValue();
+      case FLOAT:
+        float actualFloat = (float) actual;
+        float expectedFloat = ((Number) expected).floatValue();
+        if (DoubleMath.fuzzyEquals(actualFloat, expectedFloat, 
DOUBLE_CMP_EPSILON)) {
+          return true;
+        }
+        float maxFloat = Math.max(Math.abs(actualFloat), 
Math.abs(expectedFloat));
+        return DoubleMath.fuzzyEquals(actualFloat / maxFloat, expectedFloat / 
maxFloat, DOUBLE_CMP_EPSILON);
+      case DOUBLE:
+        double actualDouble = (double) actual;
+        double expectedDouble = ((Number) expected).doubleValue();
+        if (DoubleMath.fuzzyEquals(actualDouble, expectedDouble, 
DOUBLE_CMP_EPSILON)) {
+          return true;
+        }
+        double maxDouble = Math.max(Math.abs(actualDouble), 
Math.abs(expectedDouble));
+        return DoubleMath.fuzzyEquals(actualDouble / maxDouble, expectedDouble 
/ maxDouble, DOUBLE_CMP_EPSILON);
+      case BIG_DECIMAL:
+        // Use compare to handle different scale
+        return ((BigDecimal) actual).compareTo((BigDecimal) expected) == 0;
+      case BOOLEAN:
+      case TIMESTAMP:
+      case STRING:
+      case BYTES:
+        return actual.equals(expected);
+      case INT_ARRAY:
+        return Arrays.equals((int[]) actual, (int[]) expected);
+      case STRING_ARRAY:
+        return Arrays.equals((String[]) actual, (String[]) expected);
+      default:
+        throw new UnsupportedOperationException("Unsupported ColumnDataType: " 
+ columnDataType);
     }
   }
 
@@ -398,13 +433,13 @@ public abstract class QueryRunnerTestBase extends 
QueryTestSet {
   protected Connection _h2Connection;
 
   protected Connection getH2Connection() {
-    Assert.assertNotNull(_h2Connection, "H2 Connection has not been 
initialized");
+    assertNotNull(_h2Connection, "H2 Connection has not been initialized");
     return _h2Connection;
   }
 
   protected void setH2Connection()
       throws Exception {
-    Assert.assertNull(_h2Connection);
+    assertNull(_h2Connection);
     Class.forName("org.h2.Driver");
     _h2Connection = DriverManager.getConnection("jdbc:h2:mem:");
   }
diff --git 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/ResourceBasedQueriesTest.java
 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/ResourceBasedQueriesTest.java
index 16599c2bd4..1f48a2827a 100644
--- 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/ResourceBasedQueriesTest.java
+++ 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/ResourceBasedQueriesTest.java
@@ -26,7 +26,6 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.URL;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -37,12 +36,12 @@ import java.util.Set;
 import java.util.TimeZone;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.pinot.common.datatable.DataTable;
 import org.apache.pinot.common.response.broker.BrokerResponseNativeV2;
 import org.apache.pinot.common.response.broker.BrokerResponseStats;
+import org.apache.pinot.common.response.broker.ResultTable;
 import org.apache.pinot.core.common.datatable.DataTableBuilderFactory;
 import org.apache.pinot.core.query.reduce.ExecutionStatsAggregator;
 import org.apache.pinot.query.QueryEnvironmentTestBase;
@@ -57,6 +56,7 @@ import org.apache.pinot.spi.data.readers.GenericRow;
 import org.apache.pinot.spi.env.PinotConfiguration;
 import org.apache.pinot.spi.utils.BooleanUtils;
 import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.spi.utils.JsonUtils;
 import org.apache.pinot.spi.utils.builder.TableNameBuilder;
 import org.testng.Assert;
 import org.testng.annotations.AfterClass;
@@ -254,9 +254,9 @@ public class ResourceBasedQueriesTest extends 
QueryRunnerTestBase {
       boolean keepOutputRowOrder)
       throws Exception {
     // query pinot
-    runQuery(sql, expect, null).ifPresent(rows -> {
+    runQuery(sql, expect, null).ifPresent(resultTable -> {
       try {
-        compareRowEquals(rows, queryH2(h2Sql), keepOutputRowOrder);
+        compareRowEquals(resultTable, queryH2(h2Sql), keepOutputRowOrder);
       } catch (Exception e) {
         Assert.fail(e.getMessage(), e);
       }
@@ -267,7 +267,8 @@ public class ResourceBasedQueriesTest extends 
QueryRunnerTestBase {
   public void testQueryTestCasesWithOutput(String testCaseName, boolean 
isIgnored, String sql, String h2Sql,
       List<Object[]> expectedRows, String expect, boolean keepOutputRowOrder)
       throws Exception {
-    runQuery(sql, expect, null).ifPresent(rows -> compareRowEquals(rows, 
expectedRows, keepOutputRowOrder));
+    runQuery(sql, expect, null).ifPresent(
+        resultTable -> compareRowEquals(resultTable, expectedRows, 
keepOutputRowOrder));
   }
 
   @Test(dataProvider = "testResourceQueryTestCaseProviderWithMetadata")
@@ -275,7 +276,7 @@ public class ResourceBasedQueriesTest extends 
QueryRunnerTestBase {
       String expect, int numSegments)
       throws Exception {
     Map<Integer, ExecutionStatsAggregator> executionStatsAggregatorMap = new 
HashMap<>();
-    runQuery(sql, expect, executionStatsAggregatorMap).ifPresent(rows -> {
+    runQuery(sql, expect, executionStatsAggregatorMap).ifPresent(resultTable 
-> {
       BrokerResponseNativeV2 brokerResponseNative = new 
BrokerResponseNativeV2();
       executionStatsAggregatorMap.get(0).setStats(brokerResponseNative);
       Assert.assertFalse(executionStatsAggregatorMap.isEmpty());
@@ -325,17 +326,15 @@ public class ResourceBasedQueriesTest extends 
QueryRunnerTestBase {
     });
   }
 
-  private Optional<List<Object[]>> runQuery(String sql, final String except,
-      Map<Integer, ExecutionStatsAggregator> executionStatsAggregatorMap) {
+  private Optional<ResultTable> runQuery(String sql, final String except,
+      Map<Integer, ExecutionStatsAggregator> executionStatsAggregatorMap)
+      throws Exception {
     try {
       // query pinot
-      List<Object[]> resultRows = queryRunner(sql, 
executionStatsAggregatorMap);
-
-      Assert.assertNull(except,
-          "Expected error with message '" + except + "'. But instead rows were 
returned: " + resultRows.stream()
-              .map(Arrays::toString).collect(Collectors.joining(",\n")));
-
-      return Optional.of(resultRows);
+      ResultTable resultTable = queryRunner(sql, executionStatsAggregatorMap);
+      Assert.assertNull(except, "Expected error with message '" + except + "'. 
But instead rows were returned: "
+          + JsonUtils.objectToPrettyString(resultTable));
+      return Optional.of(resultTable);
     } catch (Exception e) {
       if (except == null) {
         throw e;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to