xiangfu0 commented on code in PR #20245:
URL: https://github.com/apache/pinot/pull/20245#discussion_r4555913340


##########
pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/tpch/TPCHQueryIntegrationTest.java:
##########
@@ -18,146 +18,295 @@
  */
 package org.apache.pinot.integration.tests.tpch;
 
-import com.google.common.collect.ImmutableSet;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
-import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.sql.ResultSet;
 import java.sql.Statement;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.avro.file.DataFileReader;
+import org.apache.avro.file.DataFileWriter;
+import org.apache.avro.generic.GenericDatumReader;
+import org.apache.avro.generic.GenericDatumWriter;
+import org.apache.avro.generic.GenericRecord;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.IOUtils;
 import org.apache.pinot.client.ResultSetGroup;
+import org.apache.pinot.common.utils.config.TagNameUtils;
 import org.apache.pinot.integration.tests.BaseClusterIntegrationTest;
 import org.apache.pinot.integration.tests.ClusterIntegrationTestUtils;
+import org.apache.pinot.segment.spi.partition.PartitionFunction;
+import org.apache.pinot.segment.spi.partition.PartitionFunctionFactory;
+import org.apache.pinot.spi.config.table.ColumnPartitionConfig;
+import org.apache.pinot.spi.config.table.SegmentPartitionConfig;
 import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.assignment.InstanceAssignmentConfig;
+import org.apache.pinot.spi.config.table.assignment.InstanceConstraintConfig;
+import org.apache.pinot.spi.config.table.assignment.InstancePartitionsType;
+import 
org.apache.pinot.spi.config.table.assignment.InstanceReplicaGroupPartitionConfig;
+import org.apache.pinot.spi.config.table.assignment.InstanceTagPoolConfig;
 import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.utils.builder.TableNameBuilder;
 import org.apache.pinot.tools.utils.JarUtils;
 import org.apache.pinot.util.TestUtils;
 import org.testng.Assert;
+import org.testng.SkipException;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
-// TODO: extract common functions from TPCHQueryIntegrationTest and 
SSBQueryIntegrationTest
-/// Integration test that tests Pinot using TPCH data.
-/// Data is loaded into Pinot and H2 from /resources/examples/batch/tpch. The 
dataset size is very small, please follow
-/// REAME.md to generate a larger dataset for better testing.
-/// Queries are executed against Pinot and H2, and the results are compared.
+
+/// Runs the 22 TPC-H queries on the multi-stage engine and compares the 
results with H2.
+///
+/// The data is the TPC-H data at scale factor 0.01, generated with `dbgen` 
(see `README.md` in this package). The
+/// data and the queries are in `examples/batch/tpch` of `pinot-tools`, the 
same files that the TPC-H quickstart uses.
+///
+/// Each query runs in two forms:
+/// - `queries/<n>.sql` is the query text from the TPC-H specification, with 
the default substitution parameters.
+/// - `queries/rewrites/<n>.sql` is the same query, rewritten with a join 
order and join hints that suit the
+///   multi-stage engine (see `queries/README.md`). The hints trust the table 
layout, so the test uses the layout
+///   that the rewrites need: two servers, a full copy of the small tables on 
each server, and `orders` and
+///   `lineitem` partitioned and colocated by order key. When the layout and 
the hints disagree, the query fails or
+///   returns wrong results, and the test fails in both cases.
+///
+/// H2 runs the specification text, and both forms must return the same rows 
as H2, in the same order.
 public class TPCHQueryIntegrationTest extends BaseClusterIntegrationTest {
-  private static final int NUM_TPCH_QUERIES = 24;
+  private static final int NUM_TPCH_QUERIES = 22;
+  private static final String QUERY_FOLDER = "examples/batch/tpch/queries/";
+  private static final int NUM_SERVERS = 2;
+  private static final int NUM_ORDERKEY_PARTITIONS = 4;
+  private static final String ORDERKEY_PARTITION_FUNCTION = "Murmur";
+  private static final Set<String> REPLICATED_TABLES = Set.of("customer", 
"nation", "part", "region", "supplier");
+  private static final Map<String, String> ORDERKEY_PARTITIONED_TABLES =
+      Map.of("lineitem", "l_orderkey", "orders", "o_orderkey");
+  // Number of rows of each table at scale factor 0.01.
+  private static final Map<String, Long> NUM_ROWS = Map.of("customer", 1_500L, 
"lineitem", 60_175L, "nation", 25L,
+      "orders", 15_000L, "part", 2_000L, "partsupp", 8_000L, "region", 5L, 
"supplier", 100L);
+  // The rewrites need the planner to keep the partitioning of a table scan 
through a local join.
+  private static final String REWRITE_QUERY_OPTIONS = "SET 
inferPartitionHint=true; ";
+  // Specification queries that the multi-stage engine cannot run, with the 
reason. Their rewrites still run.
+  private static final Map<Integer, String> SKIPPED_SPECIFICATION_QUERIES = 
Map.of(21,
+      "the plan for the EXISTS and NOT EXISTS subqueries builds a join with 
more than 10M rows, even at scale factor "
+          + "0.01. See queries/rewrites/21.sql");
+  // H2 has no indexes by default, and without them the joins in Q3, Q5, Q9 
and Q19 take tens of seconds.
+  private static final Map<String, List<String>> H2_INDEXED_COLUMNS = Map.of(
+      "customer", List.of("c_custkey", "c_nationkey"),
+      "lineitem", List.of("l_orderkey", "l_partkey", "l_suppkey"),
+      "nation", List.of("n_nationkey", "n_regionkey"),
+      "orders", List.of("o_orderkey", "o_custkey"),
+      "part", List.of("p_partkey"),
+      "partsupp", List.of("ps_partkey", "ps_suppkey"),
+      "region", List.of("r_regionkey"),
+      "supplier", List.of("s_suppkey", "s_nationkey"));
 
-  // Pinot queries 15, 16, 17 fail due to lack of support for views.
-  // Pinot queries 23 fail due to java heap space problem or timeout.
-  private static final Set<Integer> EXEMPT_QUERIES = ImmutableSet.of(15, 16, 
17, 23);
+  // Both forms of a query compare with the same H2 result, so each H2 query 
runs once.
+  private final Map<Integer, List<String[]>> _h2Results = new 
ConcurrentHashMap<>();
 
   @BeforeClass
   public void setUp()
       throws Exception {
     TestUtils.ensureDirectoriesExistAndEmpty(_tempDir, _segmentDir, _tarDir);
 
-    // Start the Pinot cluster
     startZk();
     startController();
     startBroker();
-    startServer();
+    startServers(NUM_SERVERS);
 
     setUpH2Connection();
     for (String tableName : Constants.TPCH_TABLE_NAMES) {
+      File resourceFolder = getTableResourceFolder(tableName);
+      File dataFile = new File(resourceFolder, "rawdata" + File.separator + 
tableName + Constants.AVRO_FILE_SUFFIX);
+      Assert.assertTrue(dataFile.exists(), "Unable to find the data file: " + 
dataFile);
+      Schema schema = createSchema(new File(resourceFolder, tableName + 
"_schema.json"));
+      addSchema(schema);
+      TableConfig tableConfig = createTableConfig(new File(resourceFolder, 
tableName + "_offline_table_config.json"));
+      applyTableLayout(tableConfig);
+      addTableConfig(tableConfig);
+
       File tableSegmentDir = new File(_segmentDir, tableName);
       File tarDir = new File(_tarDir, tableName);
-      String tableResourceFolder = Constants.getTableResourceFolder(tableName);
-      URL resourceUrl = 
getClass().getClassLoader().getResource(tableResourceFolder);
-      Assert.assertNotNull(resourceUrl, "Unable to find resource from: " + 
tableResourceFolder);
-      File resourceFile;
-      if ("jar".equals(resourceUrl.getProtocol())) {
-        String[] splits = resourceUrl.getFile().split("!");
-        File tempUnpackDir = new File(_tempDir.getAbsolutePath() + 
File.separator + splits[1]);
-        TestUtils.ensureDirectoriesExistAndEmpty(tempUnpackDir);
-        JarUtils.copyResourcesToDirectory(splits[0], splits[1].substring(1), 
tempUnpackDir.getAbsolutePath());
-        resourceFile = tempUnpackDir;
-      } else {
-        resourceFile = new File(resourceUrl.getFile());
-      }
-      File dataFile =
-          new 
File(getClass().getClassLoader().getResource(Constants.getTableAvroFilePath(tableName)).getFile());
-      Assert.assertTrue(dataFile.exists(), "Unable to load resource file from 
URL: " + dataFile);
-      File schemaFile = new File(resourceFile.getPath(), tableName + 
"_schema.json");
-      File tableFile = new File(resourceFile.getPath(), tableName + 
"_offline_table_config.json");
-      // Pinot
       TestUtils.ensureDirectoriesExistAndEmpty(tableSegmentDir, tarDir);
-      Schema schema = createSchema(schemaFile);
-      addSchema(schema);
-      TableConfig tableConfig = createTableConfig(tableFile);
-      addTableConfig(tableConfig);
-      ClusterIntegrationTestUtils.buildSegmentsFromAvro(List.of(dataFile), 
tableConfig, schema, 0,
-          tableSegmentDir, tarDir);
+      String partitionColumn = ORDERKEY_PARTITIONED_TABLES.get(tableName);
+      List<File> segmentFiles = partitionColumn == null ? List.of(dataFile)
+          : splitByOrderKeyPartition(dataFile, partitionColumn, new 
File(_tempDir, tableName + "-partitions"));
+      ClusterIntegrationTestUtils.buildSegmentsFromAvro(segmentFiles, 
tableConfig, schema, 0, tableSegmentDir, tarDir);
       uploadSegments(tableName, tarDir);
-      // H2
+
       ClusterIntegrationTestUtils.setUpH2TableWithAvro(List.of(dataFile), 
tableName, _h2Connection);
+      try (Statement h2Statement = _h2Connection.createStatement()) {
+        for (String column : H2_INDEXED_COLUMNS.get(tableName)) {
+          h2Statement.execute("CREATE INDEX idx_" + column + " ON " + 
tableName + "(" + column + ")");
+        }
+      }
+    }
+    for (String tableName : Constants.TPCH_TABLE_NAMES) {
+      long expectedNumRows = NUM_ROWS.get(tableName);
+      TestUtils.waitForCondition(aVoid -> getCurrentCountStarResult(tableName) 
== expectedNumRows, 60_000L,
+          "Failed to load " + expectedNumRows + " rows in table: " + 
tableName);
     }
   }
 
-  @Test(dataProvider = "QueryDataProvider")
-  public void testTPCHQueries(String[] pinotAndH2Queries) throws Exception {
-    testQueriesSucceed(pinotAndH2Queries[0], pinotAndH2Queries[1]);
+  private File getTableResourceFolder(String tableName)
+      throws IOException {
+    String tableResourceFolder = Constants.getTableResourceFolder(tableName);
+    URL resourceUrl = 
getClass().getClassLoader().getResource(tableResourceFolder);
+    Assert.assertNotNull(resourceUrl, "Unable to find resource from: " + 
tableResourceFolder);
+    if (!"jar".equals(resourceUrl.getProtocol())) {
+      return new File(resourceUrl.getFile());
+    }
+    String[] splits = resourceUrl.getFile().split("!");
+    File tempUnpackDir = new File(_tempDir.getAbsolutePath() + File.separator 
+ splits[1]);
+    TestUtils.ensureDirectoriesExistAndEmpty(tempUnpackDir);
+    JarUtils.copyResourcesToDirectory(splits[0], splits[1].substring(1), 
tempUnpackDir.getAbsolutePath());
+    return tempUnpackDir;
   }
 
-  protected void testQueriesSucceed(String pinotQuery, String h2Query) throws 
Exception {
-    ResultSetGroup pinotResultSetGroup = 
getPinotConnection().execute(pinotQuery);
-    org.apache.pinot.client.ResultSet resultTableResultSet = 
pinotResultSetGroup.getResultSet(0);
-    if (CollectionUtils.isNotEmpty(pinotResultSetGroup.getExceptions())) {
-      Assert.fail(String.format(
-          "TPC-H query raised exception: %s. query: %s", 
pinotResultSetGroup.getExceptions().get(0), pinotQuery));
+  /// Applies the table layout that the hints in `queries/rewrites` need:
+  /// - the small tables are replicated to every server, so a join with them 
can run where the other input is;
+  /// - `orders` and `lineitem` are partitioned by order key, and partition 
`p` of both tables is on the same server,
+  ///   so they can be joined and aggregated by order key partition by 
partition.
+  private static void applyTableLayout(TableConfig tableConfig) {
+    String tableName = 
TableNameBuilder.extractRawTableName(tableConfig.getTableName());
+    if (REPLICATED_TABLES.contains(tableName)) {
+      
tableConfig.getValidationConfig().setReplication(Integer.toString(NUM_SERVERS));
+      return;
     }
+    String partitionColumn = ORDERKEY_PARTITIONED_TABLES.get(tableName);
+    if (partitionColumn == null) {
+      return;
+    }
+    tableConfig.getIndexingConfig().setSegmentPartitionConfig(new 
SegmentPartitionConfig(
+        Map.of(partitionColumn, new 
ColumnPartitionConfig(ORDERKEY_PARTITION_FUNCTION, NUM_ORDERKEY_PARTITIONS))));
+    // One replica group with all servers. Each partition goes to one server. 
Both tables use the same instance
+    // partitions because the constraint config is empty: without it, the 
instance list is rotated by a hash of the
+    // table name, and the two tables would put the same partition on 
different servers.
+    InstanceReplicaGroupPartitionConfig replicaGroupConfig =
+        new InstanceReplicaGroupPartitionConfig(true, 0, 1, 0, 
NUM_ORDERKEY_PARTITIONS, 1, false, partitionColumn);
+    InstanceAssignmentConfig instanceAssignmentConfig = new 
InstanceAssignmentConfig(
+        new InstanceTagPoolConfig(TagNameUtils.getOfflineTagForTenant(null), 
false, 0, null),
+        new InstanceConstraintConfig(List.of()), replicaGroupConfig, null, 
false);
+    tableConfig.setInstanceAssignmentConfigMap(
+        Map.of(InstancePartitionsType.OFFLINE.name(), 
instanceAssignmentConfig));
+  }
 
-    int numRows = resultTableResultSet.getRowCount();
-    int numColumns = resultTableResultSet.getColumnCount();
+  /// Splits an Avro file into one file for each order key partition, so that 
each segment holds one partition.
+  private static List<File> splitByOrderKeyPartition(File dataFile, String 
partitionColumn, File outputDir)
+      throws IOException {
+    TestUtils.ensureDirectoriesExistAndEmpty(outputDir);
+    PartitionFunction partitionFunction =
+        
PartitionFunctionFactory.getPartitionFunction(ORDERKEY_PARTITION_FUNCTION, 
NUM_ORDERKEY_PARTITIONS, null);
+    List<File> partitionFiles = new ArrayList<>(NUM_ORDERKEY_PARTITIONS);
+    List<DataFileWriter<GenericRecord>> writers = new 
ArrayList<>(NUM_ORDERKEY_PARTITIONS);
+    try (DataFileReader<GenericRecord> reader = new DataFileReader<>(dataFile, 
new GenericDatumReader<>())) {
+      org.apache.avro.Schema avroSchema = reader.getSchema();
+      for (int partition = 0; partition < NUM_ORDERKEY_PARTITIONS; 
partition++) {
+        File partitionFile = new File(outputDir, "partition_" + partition + 
Constants.AVRO_FILE_SUFFIX);
+        DataFileWriter<GenericRecord> writer = new DataFileWriter<>(new 
GenericDatumWriter<>(avroSchema));
+        writer.create(avroSchema, partitionFile);
+        partitionFiles.add(partitionFile);
+        writers.add(writer);
+      }
+      for (GenericRecord record : reader) {
+        // The partition function reads the value as a string, which is the 
same for the Avro long and the Pinot INT.
+        int partition = 
partitionFunction.getPartition(record.get(partitionColumn).toString());
+        writers.get(partition).append(record);
+      }
+    } finally {
+      for (DataFileWriter<GenericRecord> writer : writers) {
+        writer.close();
+      }
+    }
+    return partitionFiles;
+  }
 
-    // h2 response
-    Assert.assertNotNull(_h2Connection);
-    Statement h2statement = 
_h2Connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, 
ResultSet.CONCUR_READ_ONLY);
-    h2statement.execute(h2Query);
-    ResultSet h2ResultSet = h2statement.getResultSet();
+  @Test(dataProvider = "queryNumbers")
+  public void testSpecificationQuery(int queryNumber)
+      throws Exception {
+    String skipReason = SKIPPED_SPECIFICATION_QUERIES.get(queryNumber);
+    if (skipReason != null) {
+      throw new SkipException("Q" + queryNumber + ": " + skipReason);
+    }
+    String query = readQuery(QUERY_FOLDER + queryNumber + ".sql");
+    compareWithH2(queryNumber, query);
+  }
 
-    // compare results.
-    Assert.assertEquals(numColumns, 
h2ResultSet.getMetaData().getColumnCount());
-    if (h2ResultSet.first()) {
-      for (int i = 0; i < numRows; i++) {
-        for (int c = 0; c < numColumns; c++) {
-          String h2Value = h2ResultSet.getString(c + 1);
-          String pinotValue = resultTableResultSet.getString(i, c);
-          boolean error = ClusterIntegrationTestUtils.fuzzyCompare(h2Value, 
pinotValue, pinotValue);
-          if (error) {
-            throw new RuntimeException("Value: " + c + " does not match at (" 
+ i + ", " + c + "), "
-                + "expected h2 value: " + h2Value + ", actual Pinot value: " + 
pinotValue);
-          }
+  @Test(dataProvider = "queryNumbers")
+  public void testRewrittenQuery(int queryNumber)
+      throws Exception {
+    String query = readQuery(QUERY_FOLDER + "rewrites/" + queryNumber + 
".sql");
+    compareWithH2(queryNumber, REWRITE_QUERY_OPTIONS + query);
+  }
+
+  private void compareWithH2(int queryNumber, String pinotQuery)
+      throws Exception {
+    ResultSetGroup pinotResultSetGroup = 
getPinotConnection().execute(pinotQuery);
+    if (CollectionUtils.isNotEmpty(pinotResultSetGroup.getExceptions())) {
+      Assert.fail(String.format("TPC-H Q%d raised exception: %s. query: %s", 
queryNumber,
+          pinotResultSetGroup.getExceptions().get(0), pinotQuery));
+    }
+    org.apache.pinot.client.ResultSet pinotResultSet = 
pinotResultSetGroup.getResultSet(0);
+    List<String[]> h2Rows = getH2Result(queryNumber);
+    int numRows = pinotResultSet.getRowCount();
+    int numColumns = pinotResultSet.getColumnCount();
+    Assert.assertEquals(numRows, h2Rows.size(), "Q" + queryNumber + ": number 
of rows");
+    for (int r = 0; r < numRows; r++) {
+      String[] h2Row = h2Rows.get(r);
+      Assert.assertEquals(numColumns, h2Row.length, "Q" + queryNumber + ": 
number of columns");
+      for (int c = 0; c < numColumns; c++) {
+        String pinotValue = pinotResultSet.getString(r, c);
+        if (ClusterIntegrationTestUtils.fuzzyCompare(h2Row[c], pinotValue, 
pinotValue)) {

Review Comment:
   Minor: `fuzzyCompare` uses an absolute tolerance of `max(1.0, 
|expected|ยท1e-5)`, so values below about 1 are barely checked. Q8 `mkt_share` 
is exactly `0.0` for both years at SF 0.01 (no BRAZIL supplier volume), so any 
share in [-1, 1] would pass, and the rewrite's supplier-nation join is checked 
only by the row count. Q1 `avg_disc` (~0.05) has the same problem. I checked 
the Q8 rewrite separately (it matches the spec for all 25 nations), so nothing 
is wrong today. A relative tolerance here, for example `1e-9` plus a tiny 
absolute term, would let the test catch a broken rewrite.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to