This is an automated email from the ASF dual-hosted git repository.
xiangfu0 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 b0b27d56c67 [Vector Upsert 5/5] Add FULL-upsert vector integration
coverage (#19301)
b0b27d56c67 is described below
commit b0b27d56c67950dd02c09c6fccffd334d48c588f
Author: Xiang Fu <[email protected]>
AuthorDate: Wed Aug 26 18:40:23 2026 -0700
[Vector Upsert 5/5] Add FULL-upsert vector integration coverage (#19301)
Add VectorUpsertTableTest on CustomDataQueryClusterIntegrationTest: six
physical records collapse to four current records under FULL upsert, and
vector
top-K queries must return exactly K current entities with scalar-distance
equivalence. Covers a single consuming segment and sealed-plus-consuming
segments, both query engines, two replicas, and a skipUpsert=true control
proving obsolete rows are physically nearest.
The explain assertions also pin the planned execution path per segment type:
consuming segments use the exact scan operator because the mutable vector
index
cannot restrict its search, while sealed segments use filtered ANN.
---
.../tests/custom/VectorUpsertTableTest.java | 525 +++++++++++++++++++++
1 file changed, 525 insertions(+)
diff --git
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/custom/VectorUpsertTableTest.java
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/custom/VectorUpsertTableTest.java
new file mode 100644
index 00000000000..f7679b15431
--- /dev/null
+++
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/custom/VectorUpsertTableTest.java
@@ -0,0 +1,525 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.integration.tests.custom;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.File;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.UUID;
+import org.apache.avro.file.DataFileWriter;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericDatumWriter;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.clients.consumer.KafkaConsumer;
+import org.apache.kafka.common.PartitionInfo;
+import org.apache.kafka.common.serialization.ByteArrayDeserializer;
+import org.apache.pinot.client.ExecutionStats;
+import org.apache.pinot.client.ResultSetGroup;
+import org.apache.pinot.integration.tests.ClusterIntegrationTestUtils;
+import org.apache.pinot.segment.local.data.manager.SegmentDataManager;
+import org.apache.pinot.segment.local.data.manager.TableDataManager;
+import org.apache.pinot.server.starter.helix.BaseServerStarter;
+import org.apache.pinot.spi.config.table.ColumnPartitionConfig;
+import org.apache.pinot.spi.config.table.FieldConfig;
+import org.apache.pinot.spi.config.table.ReplicaGroupStrategyConfig;
+import org.apache.pinot.spi.config.table.RoutingConfig;
+import org.apache.pinot.spi.config.table.SegmentPartitionConfig;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.config.table.UpsertConfig;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.stream.StreamConfigProperties;
+import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.spi.utils.JsonUtils;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.apache.pinot.spi.utils.builder.TableNameBuilder;
+import org.apache.pinot.util.TestUtils;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+
+/// Verifies that FULL-upsert visibility is applied before vector top-K
candidate selection for both consuming and
+/// immutable realtime segments.
+@Test(suiteName = "CustomClusterIntegrationTest")
+public class VectorUpsertTableTest extends
CustomDataQueryClusterIntegrationTest {
+ private static final String CONSUMING_TABLE_NAME =
"VectorUpsertConsumingTest";
+ private static final String SEALED_TABLE_NAME = "VectorUpsertSealedTest";
+ private static final String CONSUMING_TOPIC = CONSUMING_TABLE_NAME +
"-kafka";
+ private static final String SEALED_TOPIC = SEALED_TABLE_NAME + "-kafka";
+
+ private static final String ENTITY_ID_COLUMN = "entityId";
+ private static final String TIMESTAMP_COLUMN = "ts";
+ private static final String VECTOR_COLUMN = "embedding";
+ private static final int NUM_PARTITIONS = 1;
+ private static final int NUM_REPLICAS = 1;
+ private static final int NUM_CURRENT_RECORDS = 4;
+ private static final int NUM_PHYSICAL_RECORDS = 6;
+ private static final int TOP_K = 2;
+ private static final int REALTIME_TABLE_CONFIG_RETRY_COUNT = 5;
+ private static final long REALTIME_TABLE_CONFIG_RETRY_WAIT_MS = 1_000L;
+ private static final long KAFKA_TOPIC_METADATA_READY_TIMEOUT_MS = 30_000L;
+ private static final long STATE_TIMEOUT_MS = 120_000L;
+ private static final long OLD_TIMESTAMP = 1_700_000_000_000L;
+ private static final long NEW_TIMESTAMP = OLD_TIMESTAMP + 1_000L;
+
+ private static final TestRecord A1_V1 = new TestRecord("A1", OLD_TIMESTAMP,
1.0f, 0.0f);
+ private static final TestRecord A2_V1 = new TestRecord("A2", OLD_TIMESTAMP,
1.0f, 0.0f);
+ private static final TestRecord B1 = new TestRecord("B1", OLD_TIMESTAMP,
0.0f, 1.0f);
+ private static final TestRecord B2 = new TestRecord("B2", OLD_TIMESTAMP,
0.0f, -1.0f);
+ private static final TestRecord A1_V2 = new TestRecord("A1", NEW_TIMESTAMP,
-1.0f, 0.0f);
+ private static final TestRecord A2_V2 = new TestRecord("A2", NEW_TIMESTAMP,
-1.0f, 0.0f);
+ private static final List<TestRecord> OLD_A_AND_CURRENT_B = List.of(A1_V1,
A2_V1, B1, B2);
+ private static final List<TestRecord> NEW_A = List.of(A1_V2, A2_V2);
+ private static final List<TestRecord> ALL_RECORDS = List.of(A1_V1, A2_V1,
B1, B2, A1_V2, A2_V2);
+
+ private boolean _sealedTableCreated;
+
+ @Override
+ public String getTableName() {
+ return CONSUMING_TABLE_NAME;
+ }
+
+ @Override
+ public String getKafkaTopic() {
+ return CONSUMING_TOPIC;
+ }
+
+ @Override
+ public boolean isRealtimeTable() {
+ return true;
+ }
+
+ @Override
+ protected int getNumKafkaPartitions() {
+ return NUM_PARTITIONS;
+ }
+
+ @Override
+ protected int getRealtimeSegmentFlushSize() {
+ return 100;
+ }
+
+ @Override
+ protected String getPartitionColumn() {
+ return ENTITY_ID_COLUMN;
+ }
+
+ @Override
+ protected long getCountStarResult() {
+ return NUM_CURRENT_RECORDS;
+ }
+
+ @Override
+ public Schema createSchema() {
+ return createPinotSchema(CONSUMING_TABLE_NAME);
+ }
+
+ @Override
+ public List<File> createAvroFiles() {
+ // setUpTable() builds three purpose-specific Avro files instead of using
the default fixture path.
+ return List.of();
+ }
+
+ @Override
+ protected void setUpTable()
+ throws Exception {
+ disableMultiStageQueryEngine();
+
+ File allRecordsFile = writeAvroFile("all-records.avro", ALL_RECORDS);
+ File oldAndCurrentRecordsFile = writeAvroFile("old-a-and-current-b.avro",
OLD_A_AND_CURRENT_B);
+ File newARecordsFile = writeAvroFile("new-a.avro", NEW_A);
+
+ addSchema(createPinotSchema(CONSUMING_TABLE_NAME));
+ addSchema(createPinotSchema(SEALED_TABLE_NAME));
+ createSharedKafkaTopic(CONSUMING_TOPIC, NUM_PARTITIONS);
+ createSharedKafkaTopic(SEALED_TOPIC, NUM_PARTITIONS);
+ waitForKafkaTopicMetadataReadyForConsumer(CONSUMING_TOPIC);
+ waitForKafkaTopicMetadataReadyForConsumer(SEALED_TOPIC);
+
+ // Both tables use the same Avro field schema, so one sample file is
sufficient for both decoder instances.
+ AvroFileSchemaKafkaAvroMessageDecoder._avroFile = allRecordsFile;
+
addRealtimeTableConfigWithRetry(createRealtimeUpsertTableConfig(CONSUMING_TABLE_NAME,
CONSUMING_TOPIC),
+ CONSUMING_TOPIC);
+
addRealtimeTableConfigWithRetry(createRealtimeUpsertTableConfig(SEALED_TABLE_NAME,
SEALED_TOPIC), SEALED_TOPIC);
+ _sealedTableCreated = true;
+
+ pushAvroFiles(CONSUMING_TOPIC, List.of(allRecordsFile));
+ pushAvroFiles(SEALED_TOPIC, List.of(oldAndCurrentRecordsFile));
+
+ // The old A versions and current B records must be fully queryable before
sealing S0.
+ waitForTableState(SEALED_TABLE_NAME, OLD_A_AND_CURRENT_B.size(),
OLD_A_AND_CURRENT_B.size(), 1, 1,
+ STATE_TIMEOUT_MS);
+ forceCommitAndWait(SEALED_TABLE_NAME);
+ pushAvroFiles(SEALED_TOPIC, List.of(newARecordsFile));
+ }
+
+ @Override
+ protected void waitForAllDocsLoaded(long timeoutMs) {
+ disableMultiStageQueryEngine();
+ long effectiveTimeoutMs = Math.max(timeoutMs, STATE_TIMEOUT_MS);
+ waitForTableState(CONSUMING_TABLE_NAME, NUM_PHYSICAL_RECORDS,
NUM_CURRENT_RECORDS, 1, 1,
+ effectiveTimeoutMs);
+ waitForTableState(SEALED_TABLE_NAME, NUM_PHYSICAL_RECORDS,
NUM_CURRENT_RECORDS, 2, 1, effectiveTimeoutMs);
+ }
+
+ @AfterClass
+ @Override
+ public void tearDown()
+ throws IOException {
+ try {
+ if (_sealedTableCreated) {
+ dropRealtimeTable(SEALED_TABLE_NAME);
+ }
+ } finally {
+ super.tearDown();
+ }
+ }
+
+ @Test(dataProvider = "useBothQueryEngines")
+ public void testFullUpsertVectorCandidates(boolean useMultiStageQueryEngine)
+ throws Exception {
+ setUseMultiStageQueryEngine(useMultiStageQueryEngine);
+ assertVectorResultMatchesCurrentCorpus(CONSUMING_TABLE_NAME,
useMultiStageQueryEngine);
+ assertVectorResultMatchesCurrentCorpus(SEALED_TABLE_NAME,
useMultiStageQueryEngine);
+ }
+
+ private void assertVectorResultMatchesCurrentCorpus(String tableName,
boolean useMultiStageQueryEngine)
+ throws Exception {
+ String queryVector = "ARRAY[1.0, 0.0]";
+ String vectorQuery = String.format(
+ "SELECT %s, cosineDistance(%s, %s) AS distance FROM %s "
+ + "WHERE vectorSimilarity(%s, %s, %d) "
+ + "ORDER BY distance ASC, %s ASC LIMIT %d",
+ ENTITY_ID_COLUMN, VECTOR_COLUMN, queryVector, tableName,
+ VECTOR_COLUMN, queryVector, TOP_K, ENTITY_ID_COLUMN, TOP_K);
+ String exactQuery = String.format(
+ "SELECT %s, cosineDistance(%s, %s) AS distance FROM %s "
+ + "ORDER BY distance ASC, %s ASC LIMIT %d",
+ ENTITY_ID_COLUMN, VECTOR_COLUMN, queryVector, tableName,
ENTITY_ID_COLUMN, TOP_K);
+ String physicalControlQuery = String.format(
+ "SELECT %s, %s, cosineDistance(%s, %s) AS distance FROM %s "
+ + "ORDER BY distance ASC, %s ASC LIMIT %d OPTION(skipUpsert=true)",
+ ENTITY_ID_COLUMN, TIMESTAMP_COLUMN, VECTOR_COLUMN, queryVector,
tableName, ENTITY_ID_COLUMN, TOP_K);
+ String explainVectorQuery = tableName.equals(SEALED_TABLE_NAME) ?
String.format(
+ "SELECT %s, cosineDistance(%s, %s) AS distance FROM %s "
+ + "WHERE vectorSimilarity(%s, %s, %d) AND %s = %d "
+ + "ORDER BY distance ASC, %s ASC LIMIT %d",
+ ENTITY_ID_COLUMN, VECTOR_COLUMN, queryVector, tableName,
+ VECTOR_COLUMN, queryVector, TOP_K, TIMESTAMP_COLUMN, OLD_TIMESTAMP,
ENTITY_ID_COLUMN, TOP_K) : vectorQuery;
+
+ JsonNode vectorResponse = postQuery(vectorQuery);
+ JsonNode exactResponse = postQuery(exactQuery);
+ JsonNode physicalControlResponse = postQuery(physicalControlQuery);
+ JsonNode explainResponse = postQuery("SET explainAskingServers=true;
EXPLAIN PLAN FOR " + explainVectorQuery);
+ assertNoError(vectorResponse);
+ assertNoError(exactResponse);
+ assertNoError(physicalControlResponse);
+ assertNoError(explainResponse);
+ List<String> vectorEntityIds = extractEntityIds(vectorResponse);
+ List<String> exactEntityIds = extractEntityIds(exactResponse);
+
+ String engine = useMultiStageQueryEngine ? "multi-stage" : "single-stage";
+ assertEquals(vectorEntityIds.size(), TOP_K,
+ "Vector query must return exactly K rows for " + tableName + " on the
" + engine + " engine");
+ assertEquals(vectorEntityIds, List.of("B1", "B2"),
+ "Obsolete A versions must not consume vector candidates for " +
tableName + " on the " + engine
+ + " engine");
+ assertEquals(vectorEntityIds, exactEntityIds,
+ "Vector and exact scalar ranking must agree for " + tableName + " on
the " + engine + " engine");
+
+ JsonNode physicalRows = getRows(physicalControlResponse);
+ assertEquals(physicalRows.size(), TOP_K,
+ "skipUpsert control must return exactly K physical rows for " +
tableName + " on the " + engine + " engine");
+ assertEquals(extractEntityIds(physicalControlResponse), List.of("A1",
"A2"),
+ "The obsolete A versions should be the physically nearest rows for " +
tableName + " on the " + engine
+ + " engine");
+ for (JsonNode row : physicalRows) {
+ assertEquals(row.get(1).asLong(), OLD_TIMESTAMP, "skipUpsert must expose
the obsolete A version");
+ assertEquals(row.get(2).asDouble(), 0.0, 1e-6, "The obsolete A vector
must be an exact query match");
+ }
+
+ String explain = GroupByOptionsTest.toExplainStr(explainResponse,
useMultiStageQueryEngine);
+ assertExplainContains(explain, "requiredDocIdFilterApplied", true);
+ if (tableName.equals(CONSUMING_TABLE_NAME)) {
+ // The mutable vector index cannot restrict its search, so the planner
selects the exact scan operator.
+ assertTrue(explain.contains("VECTOR_SIMILARITY_EXACT_SCAN") ||
explain.contains("VectorSimilarityExactScan"),
+ "Consuming segments must fall back to the exact scan operator: " +
explain);
+ assertExplainContains(explain, "fallbackReason",
"mutable_vector_index_not_filter_aware");
+ } else {
+ assertTrue(explain.contains("VECTOR_SIMILARITY_INDEX") ||
explain.contains("VectorSimilarityIndex"),
+ "Sealed segments must use their vector index: " + explain);
+ assertExplainContains(explain, "searchMode", "FILTER_THEN_ANN");
+ }
+ }
+
+ private static void assertExplainContains(String explain, String key, Object
value) {
+ String legacyStyle = key + ":" + value;
+ String multiStageStyle = key + "=[" + value + "]";
+ assertTrue(explain.contains(legacyStyle) ||
explain.contains(multiStageStyle),
+ "Explain should contain " + key + '=' + value + ": " + explain);
+ }
+
+ private TableConfig createRealtimeUpsertTableConfig(String tableName, String
topicName) {
+ UpsertConfig upsertConfig = new UpsertConfig(UpsertConfig.Mode.FULL);
+ upsertConfig.setComparisonColumns(List.of(TIMESTAMP_COLUMN));
+
+ Map<String, String> streamConfigs = getStreamConfigMap();
+ streamConfigs.put(StreamConfigProperties.constructStreamProperty("kafka",
+ StreamConfigProperties.STREAM_TOPIC_NAME), topicName);
+
+ FieldConfig vectorFieldConfig = new FieldConfig.Builder(VECTOR_COLUMN)
+ .withEncodingType(FieldConfig.EncodingType.RAW)
+ .withIndexTypes(List.of(FieldConfig.IndexType.VECTOR))
+ .withProperties(Map.of(
+ "vectorIndexType", "HNSW",
+ "vectorDimension", "2",
+ "vectorDistanceFunction", "COSINE",
+ "version", "1",
+ "commitDocs", "1"))
+ .build();
+
+ return new TableConfigBuilder(TableType.REALTIME)
+ .setTableName(tableName)
+ .setTimeColumnName(TIMESTAMP_COLUMN)
+ .setNumReplicas(NUM_REPLICAS)
+ .setFieldConfigList(List.of(vectorFieldConfig))
+ .setStreamConfigs(streamConfigs)
+ .setUpsertConfig(upsertConfig)
+ .setRoutingConfig(new RoutingConfig(null, null,
+ RoutingConfig.STRICT_REPLICA_GROUP_INSTANCE_SELECTOR_TYPE, false))
+ .setSegmentPartitionConfig(new SegmentPartitionConfig(
+ Map.of(ENTITY_ID_COLUMN, new ColumnPartitionConfig("Murmur",
NUM_PARTITIONS))))
+ .setReplicaGroupStrategyConfig(new
ReplicaGroupStrategyConfig(ENTITY_ID_COLUMN, 1))
+ .build();
+ }
+
+ private void addRealtimeTableConfigWithRetry(TableConfig tableConfig, String
topicName)
+ throws Exception {
+ for (int attempt = 1; attempt <= REALTIME_TABLE_CONFIG_RETRY_COUNT;
attempt++) {
+ try {
+ addTableConfig(tableConfig);
+ return;
+ } catch (IOException e) {
+ if (!isRetryableRealtimePartitionMetadataError(e, topicName)
+ || attempt == REALTIME_TABLE_CONFIG_RETRY_COUNT) {
+ throw e;
+ }
+ LOGGER.warn("Retrying realtime table creation for topic {} after
metadata propagation failure "
+ + "(attempt {}/{})", topicName, attempt,
REALTIME_TABLE_CONFIG_RETRY_COUNT, e);
+ waitForKafkaTopicMetadataReadyForConsumer(topicName);
+ Thread.sleep(REALTIME_TABLE_CONFIG_RETRY_WAIT_MS);
+ }
+ }
+ throw new IllegalStateException("Failed to create realtime table after
retries for topic: " + topicName);
+ }
+
+ private static boolean isRetryableRealtimePartitionMetadataError(Throwable
throwable, String topicName) {
+ String errorToken = "Failed to fetch partition information for topic: " +
topicName;
+ Throwable current = throwable;
+ while (current != null) {
+ String message = current.getMessage();
+ if (message != null && message.contains(errorToken)) {
+ return true;
+ }
+ current = current.getCause();
+ }
+ return false;
+ }
+
+ private void waitForKafkaTopicMetadataReadyForConsumer(String topicName) {
+ TestUtils.waitForCondition(aVoid ->
isKafkaTopicMetadataReadyForConsumer(topicName), 200L,
+ KAFKA_TOPIC_METADATA_READY_TIMEOUT_MS,
+ "Kafka topic '" + topicName + "' metadata is not visible to consumers
in custom cluster suite");
+ }
+
+ private boolean isKafkaTopicMetadataReadyForConsumer(String topicName) {
+ Properties consumerProps = new Properties();
+ consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
getSharedKafkaBrokerList());
+ consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG,
"pinot-vector-upsert-topic-ready-" + UUID.randomUUID());
+ consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
ByteArrayDeserializer.class.getName());
+ consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
ByteArrayDeserializer.class.getName());
+ consumerProps.put(ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG, "5000");
+ consumerProps.put(ConsumerConfig.DEFAULT_API_TIMEOUT_MS_CONFIG, "5000");
+ try (KafkaConsumer<byte[], byte[]> consumer = new
KafkaConsumer<>(consumerProps)) {
+ List<PartitionInfo> partitionInfos = consumer.partitionsFor(topicName,
Duration.ofSeconds(5));
+ return partitionInfos != null && partitionInfos.size() >= NUM_PARTITIONS;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ private void pushAvroFiles(String topicName, List<File> avroFiles)
+ throws Exception {
+ ClusterIntegrationTestUtils.pushAvroIntoKafka(avroFiles,
getSharedKafkaBrokerList(), topicName,
+ getMaxNumKafkaMessagesPerBatch(), getKafkaMessageHeader(),
ENTITY_ID_COLUMN, false);
+ }
+
+ private void forceCommitAndWait(String tableName)
+ throws Exception {
+ String realtimeTableName =
TableNameBuilder.REALTIME.tableNameWithType(tableName);
+ String response =
getOrCreateAdminClient().getTableClient().forceCommit(realtimeTableName);
+ String jobId =
JsonUtils.stringToJsonNode(response).get("forceCommitJobId").asText();
+
+ TestUtils.waitForCondition(aVoid -> {
+ try {
+ String status =
getOrCreateAdminClient().getTableClient().getForceCommitJobStatus(jobId);
+ return JsonUtils.stringToJsonNode(status)
+
.path(CommonConstants.ControllerJob.NUM_CONSUMING_SEGMENTS_YET_TO_BE_COMMITTED).asInt(-1)
== 0;
+ } catch (Exception e) {
+ return false;
+ }
+ }, 200L, STATE_TIMEOUT_MS, "Timed out waiting for force-commit job: " +
jobId);
+ }
+
+ private void waitForTableState(String tableName, int expectedPhysicalDocs,
int expectedCurrentDocs,
+ int expectedSegments, int expectedConsumingSegments, long timeoutMs) {
+ TestUtils.waitForCondition(aVoid -> {
+ try {
+ if (!areReplicasReady(tableName, expectedSegments)) {
+ return false;
+ }
+ // Query repeatedly after verifying the table data manager so document
counts and segment state must remain
+ // stable across broker requests.
+ for (int i = 0; i < 10; i++) {
+ ResultSetGroup physicalResult = getPinotConnection().execute(
+ "SELECT COUNT(*) FROM " + tableName + "
OPTION(skipUpsert=true)");
+ ResultSetGroup currentResult = getPinotConnection().execute("SELECT
COUNT(*) FROM " + tableName);
+ if (physicalResult.getResultSet(0).getLong(0) != expectedPhysicalDocs
+ || currentResult.getResultSet(0).getLong(0) !=
expectedCurrentDocs) {
+ return false;
+ }
+ ExecutionStats executionStats = physicalResult.getExecutionStats();
+ if (executionStats.getNumSegmentsQueried() != expectedSegments
+ || executionStats.getNumConsumingSegmentsQueried() !=
expectedConsumingSegments) {
+ return false;
+ }
+ }
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }, 200L, timeoutMs,
+ String.format("Timed out waiting for %s: physicalDocs=%d,
currentDocs=%d, segments=%d, consumingSegments=%d",
+ tableName, expectedPhysicalDocs, expectedCurrentDocs,
expectedSegments, expectedConsumingSegments));
+ }
+
+ private boolean areReplicasReady(String tableName, int expectedSegments) {
+ String realtimeTableName =
TableNameBuilder.REALTIME.tableNameWithType(tableName);
+ int readyReplicas = 0;
+ for (BaseServerStarter serverStarter : getSharedServerStarters()) {
+ TableDataManager tableDataManager =
serverStarter.getServerInstance().getInstanceDataManager()
+ .getTableDataManager(realtimeTableName);
+ if (tableDataManager == null) {
+ continue;
+ }
+ List<SegmentDataManager> segmentDataManagers =
tableDataManager.acquireAllSegments();
+ try {
+ if (segmentDataManagers.size() == expectedSegments) {
+ readyReplicas++;
+ }
+ } finally {
+ for (SegmentDataManager segmentDataManager : segmentDataManagers) {
+ tableDataManager.releaseSegment(segmentDataManager);
+ }
+ }
+ }
+ return readyReplicas == NUM_REPLICAS;
+ }
+
+ private File writeAvroFile(String fileName, List<TestRecord> records)
+ throws IOException {
+ org.apache.avro.Schema avroSchema = createAvroSchema();
+ File avroFile = new File(_tempDir, fileName);
+ try (DataFileWriter<GenericData.Record> writer =
+ new DataFileWriter<>(new GenericDatumWriter<>(avroSchema))) {
+ writer.create(avroSchema, avroFile);
+ for (TestRecord testRecord : records) {
+ GenericData.Record record = new GenericData.Record(avroSchema);
+ record.put(ENTITY_ID_COLUMN, testRecord._entityId);
+ record.put(TIMESTAMP_COLUMN, testRecord._timestamp);
+ record.put(VECTOR_COLUMN, List.of(testRecord._vectorX,
testRecord._vectorY));
+ writer.append(record);
+ }
+ }
+ return avroFile;
+ }
+
+ private static Schema createPinotSchema(String tableName) {
+ return new Schema.SchemaBuilder()
+ .setSchemaName(tableName)
+ .addSingleValueDimension(ENTITY_ID_COLUMN, FieldSpec.DataType.STRING)
+ .addDateTime(TIMESTAMP_COLUMN, FieldSpec.DataType.LONG,
"1:MILLISECONDS:EPOCH", "1:MILLISECONDS")
+ .addMultiValueDimension(VECTOR_COLUMN, FieldSpec.DataType.FLOAT)
+ .setPrimaryKeyColumns(List.of(ENTITY_ID_COLUMN))
+ .build();
+ }
+
+ private static org.apache.avro.Schema createAvroSchema() {
+ org.apache.avro.Schema avroSchema =
+ org.apache.avro.Schema.createRecord("VectorUpsertRecord", null, null,
false);
+ avroSchema.setFields(List.of(
+ new org.apache.avro.Schema.Field(ENTITY_ID_COLUMN,
+ org.apache.avro.Schema.create(org.apache.avro.Schema.Type.STRING),
null, null),
+ new org.apache.avro.Schema.Field(TIMESTAMP_COLUMN,
+ org.apache.avro.Schema.create(org.apache.avro.Schema.Type.LONG),
null, null),
+ new org.apache.avro.Schema.Field(VECTOR_COLUMN,
+
org.apache.avro.Schema.createArray(org.apache.avro.Schema.create(org.apache.avro.Schema.Type.FLOAT)),
+ null, null)));
+ return avroSchema;
+ }
+
+ private static List<String> extractEntityIds(JsonNode response) {
+ JsonNode rows = getRows(response);
+ List<String> entityIds = new ArrayList<>(rows.size());
+ for (JsonNode row : rows) {
+ entityIds.add(row.get(0).asText());
+ }
+ return entityIds;
+ }
+
+ private static JsonNode getRows(JsonNode response) {
+ JsonNode resultTable = response.get("resultTable");
+ assertNotNull(resultTable, "Query failed: " + response.toPrettyString());
+ return resultTable.get("rows");
+ }
+
+ private static final class TestRecord {
+ private final String _entityId;
+ private final long _timestamp;
+ private final float _vectorX;
+ private final float _vectorY;
+
+ private TestRecord(String entityId, long timestamp, float vectorX, float
vectorY) {
+ _entityId = entityId;
+ _timestamp = timestamp;
+ _vectorX = vectorX;
+ _vectorY = vectorY;
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]