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 678c99cd308 Add enableMaterializedViewRewrite per-query option to
control MV rewrite (#18665)
678c99cd308 is described below
commit 678c99cd3088a42f8fce264db088207cc3ba548f
Author: Hongkun Xu <[email protected]>
AuthorDate: Sat Jun 6 10:01:00 2026 +0800
Add enableMaterializedViewRewrite per-query option to control MV rewrite
(#18665)
Introduce a user-facing per-query option `enableMaterializedViewRewrite`
(default true) that gates broker-side materialized-view rewrite. The MV
minion query executor forces `enableMaterializedViewRewrite=false` for its
materialization query via gRPC request metadata, which the broker applies as
an override after parsing the query. This makes the disable unconditional:
it
cannot be defeated by an `enableMaterializedViewRewrite` option already
present in the (user-authored) materialization SQL, since a SQL `SET` prefix
would otherwise lose to a later duplicate and SQL options outrank request
options. The materialization query therefore always reads the base table and
is never rewritten back onto an MV (its own or a sibling), avoiding the
circular-rewrite hazard. Disabling only forgoes an optimization and never
changes results. Distinct from the broker-internal materializedViewRewrite
marker, which remains stripped from user input.
Signed-off-by: Hongkun Xu <[email protected]>
Co-authored-by: Xiang Fu <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
.../apache/pinot/broker/grpc/BrokerGrpcServer.java | 14 +++
.../BaseSingleStageBrokerRequestHandler.java | 6 +-
.../pinot/broker/grpc/BrokerGrpcServerTest.java | 42 +++++++
.../BaseSingleStageBrokerRequestHandlerTest.java | 121 +++++++++++++++++++++
.../common/utils/config/QueryOptionsUtils.java | 12 ++
.../common/utils/config/QueryOptionsUtilsTest.java | 20 ++++
.../GrpcMaterializedViewQueryExecutor.java | 29 ++++-
.../GrpcMaterializedViewQueryExecutorTest.java | 24 ++++
.../apache/pinot/spi/utils/CommonConstants.java | 10 ++
9 files changed, 273 insertions(+), 5 deletions(-)
diff --git
a/pinot-broker/src/main/java/org/apache/pinot/broker/grpc/BrokerGrpcServer.java
b/pinot-broker/src/main/java/org/apache/pinot/broker/grpc/BrokerGrpcServer.java
index ed8db8261bf..639bfab917a 100644
---
a/pinot-broker/src/main/java/org/apache/pinot/broker/grpc/BrokerGrpcServer.java
+++
b/pinot-broker/src/main/java/org/apache/pinot/broker/grpc/BrokerGrpcServer.java
@@ -247,6 +247,20 @@ public class BrokerGrpcServer extends
PinotQueryBrokerGrpc.PinotQueryBrokerImplB
return;
}
+ /// The MV minion executor sets this request-metadata flag to
force-disable materialized-view
+ /// rewrite for its materialization query. Apply it as an override AFTER
parsing so it cannot be
+ /// defeated by an `enableMaterializedViewRewrite` option present in the
(user-authored) query
+ /// text: the parser keeps the last value for a duplicated SET, and SQL
options otherwise outrank
+ /// request options. The materialization query reads the base table and
must never be rewritten
+ /// back onto an MV (its own or a sibling). Safe to force from request
metadata — disabling MV
+ /// rewrite only forgoes an optimization and never changes results.
+ String mvRewriteOverride =
+
metadataMap.get(CommonConstants.Broker.Request.QueryOptionKey.ENABLE_MATERIALIZED_VIEW_REWRITE);
+ if (mvRewriteOverride != null) {
+ sqlNodeAndOptions.getOptions()
+
.put(CommonConstants.Broker.Request.QueryOptionKey.ENABLE_MATERIALIZED_VIEW_REWRITE,
mvRewriteOverride);
+ }
+
RequesterIdentity requesterIdentify =
GrpcRequesterIdentity.fromRequest(request);
RequestContext requestContext = new DefaultRequestContext();
BrokerResponse brokerResponse;
diff --git
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandler.java
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandler.java
index 74efd521c43..44b05308aa1 100644
---
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandler.java
+++
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandler.java
@@ -575,7 +575,11 @@ public abstract class BaseSingleStageBrokerRequestHandler
extends BaseBrokerRequ
/// response's `tablesQueried` field tags the user's base table — not the
MV the broker
/// routed through. Equals `rawTableName` when no MV swap happens.
String userRawTableName = rawTableName;
- if (_materializedViewHandler != null) {
+ /// Per-query opt-out (default true). A user — or the MV minion executor
for its own
+ /// materialization query — sets `enableMaterializedViewRewrite=false` to
force the base-table
+ /// path and avoid rewriting a materialization query back onto an MV.
+ if (_materializedViewHandler != null
+ &&
QueryOptionsUtils.isMaterializedViewRewriteEnabled(serverPinotQuery.getQueryOptions()))
{
MaterializedViewCompileOutcome outcome =
applyMaterializedViewRewriteAtCompile(
requestId, serverPinotQuery, tableName, rawTableName, schema,
_tableCache.isIgnoreCase());
materializedViewContext = outcome._materializedViewContext;
diff --git
a/pinot-broker/src/test/java/org/apache/pinot/broker/grpc/BrokerGrpcServerTest.java
b/pinot-broker/src/test/java/org/apache/pinot/broker/grpc/BrokerGrpcServerTest.java
index 649b7dee418..542b02a3e67 100644
---
a/pinot-broker/src/test/java/org/apache/pinot/broker/grpc/BrokerGrpcServerTest.java
+++
b/pinot-broker/src/test/java/org/apache/pinot/broker/grpc/BrokerGrpcServerTest.java
@@ -32,6 +32,7 @@ import org.apache.pinot.common.response.broker.ResultTable;
import org.apache.pinot.common.utils.DataSchema;
import org.apache.pinot.spi.env.PinotConfiguration;
import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.sql.parsers.SqlNodeAndOptions;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -214,6 +215,47 @@ public class BrokerGrpcServerTest {
verify(_brokerMetrics).addMeteredGlobalValue(eq(BrokerMeter.GRPC_BYTES_SENT),
anyLong());
}
+ @Test
+ public void testForceDisableMaterializedViewRewriteViaMetadataOverridesSql()
+ throws Exception {
+ /// The SQL explicitly tries to ENABLE MV rewrite; the request-metadata
flag must force it OFF,
+ /// proving the metadata override beats an `enableMaterializedViewRewrite`
option in the query
+ /// text (the exact case the MV minion executor relies on for its
materialization query).
+ Broker.BrokerRequest request = Broker.BrokerRequest.newBuilder()
+ .setSql("SET enableMaterializedViewRewrite='true'; SELECT * FROM
testTable")
+
.putMetadata(CommonConstants.Broker.Request.QueryOptionKey.ENABLE_MATERIALIZED_VIEW_REWRITE,
"false")
+ .build();
+
+ ArgumentCaptor<SqlNodeAndOptions> optionsCaptor =
ArgumentCaptor.forClass(SqlNodeAndOptions.class);
+ when(_brokerRequestHandler.handleRequest(any(), optionsCaptor.capture(),
any(), any(), any()))
+ .thenReturn(new BrokerResponseNative());
+
+ _brokerGrpcServer.submit(request, createMockStreamObserver(new
ArrayList<>()));
+
+ assertEquals(optionsCaptor.getValue().getOptions()
+
.get(CommonConstants.Broker.Request.QueryOptionKey.ENABLE_MATERIALIZED_VIEW_REWRITE),
+ "false", "Request metadata must force
enableMaterializedViewRewrite=false, overriding the SQL SET");
+ }
+
+ @Test
+ public void testNoMaterializedViewRewriteMetadataLeavesSqlOptionUntouched()
+ throws Exception {
+ /// Without the metadata flag, no override is applied and the SQL SET is
the source of truth.
+ Broker.BrokerRequest request = Broker.BrokerRequest.newBuilder()
+ .setSql("SET enableMaterializedViewRewrite='true'; SELECT * FROM
testTable")
+ .build();
+
+ ArgumentCaptor<SqlNodeAndOptions> optionsCaptor =
ArgumentCaptor.forClass(SqlNodeAndOptions.class);
+ when(_brokerRequestHandler.handleRequest(any(), optionsCaptor.capture(),
any(), any(), any()))
+ .thenReturn(new BrokerResponseNative());
+
+ _brokerGrpcServer.submit(request, createMockStreamObserver(new
ArrayList<>()));
+
+ assertEquals(optionsCaptor.getValue().getOptions()
+
.get(CommonConstants.Broker.Request.QueryOptionKey.ENABLE_MATERIALIZED_VIEW_REWRITE),
+ "true", "Without the metadata flag, the SQL-set option must be
preserved");
+ }
+
/**
* Helper method to create a mock StreamObserver that captures all responses.
*/
diff --git
a/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandlerTest.java
b/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandlerTest.java
index a791dd0f1a6..358cb557cc7 100644
---
a/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandlerTest.java
+++
b/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/BaseSingleStageBrokerRequestHandlerTest.java
@@ -81,6 +81,8 @@ import org.testng.annotations.Test;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -1197,6 +1199,125 @@ public class BaseSingleStageBrokerRequestHandlerTest {
"Response must report the MV name when the swap was committed");
}
+ /**
+ * Per-query opt-out: a query carrying {@code
enableMaterializedViewRewrite=false} (the option the
+ * MV minion executor injects onto its materialization query) MUST bypass
the rewrite path
+ * entirely. This is the regression guard for the circular-rewrite hazard:
a materialization
+ * query reads the base table, and with broker-wide MV rewrite enabled it
would otherwise be
+ * eligible to be rewritten back onto an MV over the same base table (its
own MV, or a sibling).
+ * Using the identical setup to {@link
#testMaterializedViewFullRewriteActuallySwapsServerQuery}
+ * (which DOES swap), this asserts the gate prevents the swap: the rewrite
engine is never
+ * consulted, the server query targets the base table, no MV marker is
stamped, and the response
+ * reports no MV.
+ */
+ @Test
+ public void testMaterializedViewRewriteSkippedWhenDisabledByQueryOption()
+ throws Exception {
+ String baseOfflineTable = "baseTable_OFFLINE";
+ String materializedViewOfflineTable = "mv_baseTable_OFFLINE";
+ String baseRawTable = "baseTable";
+ String materializedViewRawTable = "mv_baseTable";
+
+ String userSql = "SET enableMaterializedViewRewrite='false';"
+ + "SELECT ts, SUM(revenue) FROM baseTable GROUP BY ts LIMIT 100";
+ PinotQuery materializedViewQuery = CalciteSqlParser.compileToPinotQuery(
+ "SELECT ts, SUM(revenue) FROM mv_baseTable_OFFLINE GROUP BY ts LIMIT
100");
+
+ /// The engine WOULD return a committable FULL_REWRITE if consulted — but
the gate must ensure
+ /// it is never consulted at all.
+ MaterializedViewRewritePlan plan = new MaterializedViewRewritePlan(
+ materializedViewOfflineTable, MatchType.EXACT,
ExecutionMode.FULL_REWRITE, materializedViewQuery, 1.0);
+ MaterializedViewQueryRewriteEngine materializedViewEngine =
mock(MaterializedViewQueryRewriteEngine.class);
+ when(materializedViewEngine.tryRewrite(any(PinotQuery.class),
anyString())).thenReturn(plan);
+ MaterializedViewHandler materializedViewHandler = new
DefaultMaterializedViewHandler(materializedViewEngine);
+
+ Schema baseSchema = new Schema.SchemaBuilder()
+ .setSchemaName(baseRawTable)
+ .addSingleValueDimension("ts", DataType.STRING)
+ .addMetric("revenue", DataType.DOUBLE)
+ .build();
+
+ TableCache tableCache = mock(TableCache.class);
+ when(tableCache.getActualTableName(baseRawTable)).thenReturn(baseRawTable);
+ when(tableCache.getSchema(baseRawTable)).thenReturn(baseSchema);
+ when(tableCache.getColumnNameMap(anyString())).thenReturn(Map.of("ts",
"ts", "revenue", "revenue"));
+ TableConfig tableCfg = mock(TableConfig.class);
+ TenantConfig tenant = new TenantConfig("t_BROKER", "t_SERVER", null);
+ when(tableCfg.getTenantConfig()).thenReturn(tenant);
+ when(tableCache.getTableConfig(baseOfflineTable)).thenReturn(tableCfg);
+
when(tableCache.getTableConfig(TableNameBuilder.REALTIME.tableNameWithType(baseRawTable))).thenReturn(null);
+
+ BrokerRoutingManager routingManager = mock(BrokerRoutingManager.class);
+ when(routingManager.routingExists(baseOfflineTable)).thenReturn(true);
+ when(routingManager.getQueryTimeoutMs(anyString())).thenReturn(10000L);
+ RoutingTable rt = mock(RoutingTable.class);
+ when(rt.getServerInstanceToSegmentsMap()).thenReturn(
+ Map.of(new ServerInstance(new InstanceConfig("server01_9000")),
+ new SegmentsToQuery(List.of("seg01"), List.of())));
+ when(routingManager.getRoutingTable(any(),
Mockito.anyLong())).thenReturn(rt);
+
+ QueryQuotaManager quotaManager = mock(QueryQuotaManager.class);
+ when(quotaManager.acquireDatabase(anyString())).thenReturn(true);
+ when(quotaManager.acquireApplication(anyString())).thenReturn(true);
+ when(quotaManager.acquire(anyString())).thenReturn(true);
+
+ BrokerMetrics.register(mock(BrokerMetrics.class));
+ PinotConfiguration config = new PinotConfiguration();
+ BrokerQueryEventListenerFactory.init(config);
+
+ AtomicReference<BrokerRequest> capturedServerBrokerRequest = new
AtomicReference<>();
+ BaseSingleStageBrokerRequestHandler handler =
+ new BaseSingleStageBrokerRequestHandler(config, "broker1", new
BrokerRequestIdGenerator(),
+ routingManager, new AllowAllAccessControlFactory(), quotaManager,
tableCache,
+ ThreadAccountantUtils.getNoOpAccountant(), null,
materializedViewHandler) {
+ @Override
+ public void start() {
+ }
+
+ @Override
+ public void shutDown() {
+ }
+
+ @Override
+ protected BrokerResponseNative processBrokerRequest(long requestId,
+ BrokerRequest originalBrokerRequest, BrokerRequest
serverBrokerRequest,
+ TableRouteInfo route, long timeoutMs, ServerStats serverStats,
+ RequestContext requestContext) {
+ capturedServerBrokerRequest.set(serverBrokerRequest);
+ return BrokerResponseNative.empty();
+ }
+
+ @Override
+ protected BrokerResponseNative
processMaterializedViewSplitBrokerRequest(long requestId,
+ long materializedViewRequestId, BrokerRequest
originalBrokerRequest, TableRouteInfo baseRoute,
+ TableRouteInfo materializedViewRoute, long timeoutMs,
ServerStats serverStats,
+ RequestContext requestContext) {
+ Assert.fail("MV split path must not be reached when
enableMaterializedViewRewrite=false");
+ return BrokerResponseNative.empty();
+ }
+ };
+
+ BrokerResponseNative response = (BrokerResponseNative)
handler.handleRequest(userSql);
+ BrokerRequest serverBrokerRequest = capturedServerBrokerRequest.get();
+ Assert.assertNotNull(serverBrokerRequest,
+ "processBrokerRequest must have been reached on the base-table path;
exceptions: "
+ + response.getExceptions());
+ /// The rewrite engine must never be consulted when the per-query opt-out
is set.
+ verify(materializedViewEngine, never()).tryRewrite(any(PinotQuery.class),
anyString());
+ /// The server query MUST target the base table — no swap to the MV.
+ String serverTableName =
serverBrokerRequest.getPinotQuery().getDataSource().getTableName();
+ Assert.assertEquals(serverTableName, baseOfflineTable,
+ "Query with enableMaterializedViewRewrite=false must target the base
table but got: " + serverTableName);
+ /// No MV-rewrite marker may be stamped.
+ Map<String, String> serverOptions =
serverBrokerRequest.getPinotQuery().getQueryOptions();
+ Assert.assertTrue(serverOptions == null
+ ||
!serverOptions.containsKey(CommonConstants.Broker.Request.QueryOptionKey.MATERIALIZED_VIEW_REWRITE),
+ "MV-rewrite marker must not be stamped when rewrite is disabled;
options: " + serverOptions);
+ /// And the response must NOT report a materializedViewQueried.
+ Assert.assertNull(response.getMaterializedViewQueried(),
+ "Response must not report a materializedViewQueried when rewrite is
disabled by the query option");
+ }
+
/**
* Pins the cascade-prevention guard: when the user's query already targets
an MV table
* directly ({@code TableConfig.isMaterializedView()} is {@code true}), the
broker must skip
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
b/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
index 52829dc07f1..64bcaaa2baf 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
@@ -150,6 +150,18 @@ public class QueryOptionsUtils {
return Boolean.parseBoolean(queryOptions.get(QueryOptionKey.SKIP_UPSERT));
}
+ /// Returns whether materialized-view rewrite is allowed for this query.
Defaults to `true`
+ /// (absence ⇒ rewrite allowed) for backward compatibility with the
pre-option behavior; the
+ /// MV minion executor sets it to `false` so a materialization query is
never rewritten back
+ /// onto an MV.
+ public static boolean isMaterializedViewRewriteEnabled(Map<String, String>
queryOptions) {
+ if (queryOptions == null) {
+ return true;
+ }
+ String value =
queryOptions.get(QueryOptionKey.ENABLE_MATERIALIZED_VIEW_REWRITE);
+ return value == null || Boolean.parseBoolean(value);
+ }
+
public static boolean isSkipUpsertView(Map<String, String> queryOptions) {
return
Boolean.parseBoolean(queryOptions.get(QueryOptionKey.SKIP_UPSERT_VIEW));
}
diff --git
a/pinot-common/src/test/java/org/apache/pinot/common/utils/config/QueryOptionsUtilsTest.java
b/pinot-common/src/test/java/org/apache/pinot/common/utils/config/QueryOptionsUtilsTest.java
index 7aa346b0f3d..8ccc0b1232e 100644
---
a/pinot-common/src/test/java/org/apache/pinot/common/utils/config/QueryOptionsUtilsTest.java
+++
b/pinot-common/src/test/java/org/apache/pinot/common/utils/config/QueryOptionsUtilsTest.java
@@ -29,7 +29,9 @@ import org.testng.annotations.Test;
import static
org.apache.pinot.spi.utils.CommonConstants.Broker.Request.QueryOptionKey.*;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
@@ -69,6 +71,24 @@ public class QueryOptionsUtilsTest {
assertEquals(resolved.get(TABLE_SAMPLER), "firstOnly");
}
+ @Test
+ public void materializedViewRewriteDefaultsToEnabled() {
+ // Absent option and null map both default to enabled (back-compat with
pre-option behavior).
+ assertTrue(QueryOptionsUtils.isMaterializedViewRewriteEnabled(null));
+ assertTrue(QueryOptionsUtils.isMaterializedViewRewriteEnabled(new
HashMap<>()));
+ assertTrue(QueryOptionsUtils.isMaterializedViewRewriteEnabled(
+ Map.of(ENABLE_MATERIALIZED_VIEW_REWRITE, "true")));
+ assertTrue(QueryOptionsUtils.isMaterializedViewRewriteEnabled(
+ Map.of(ENABLE_MATERIALIZED_VIEW_REWRITE, "TRUE")));
+ // Anything that is not "true" disables (explicit false, case variants,
and any non-true value).
+ assertFalse(QueryOptionsUtils.isMaterializedViewRewriteEnabled(
+ Map.of(ENABLE_MATERIALIZED_VIEW_REWRITE, "false")));
+ assertFalse(QueryOptionsUtils.isMaterializedViewRewriteEnabled(
+ Map.of(ENABLE_MATERIALIZED_VIEW_REWRITE, "FALSE")));
+ assertFalse(QueryOptionsUtils.isMaterializedViewRewriteEnabled(
+ Map.of(ENABLE_MATERIALIZED_VIEW_REWRITE, "1")));
+ }
+
@Test
public void shouldExtractTableSamplerOption() {
assertEquals(QueryOptionsUtils.getTableSampler(Map.of(TABLE_SAMPLER,
"firstOnly")), "firstOnly");
diff --git
a/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/executor/GrpcMaterializedViewQueryExecutor.java
b/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/executor/GrpcMaterializedViewQueryExecutor.java
index f66167aedc1..83046bf8fed 100644
---
a/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/executor/GrpcMaterializedViewQueryExecutor.java
+++
b/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/executor/GrpcMaterializedViewQueryExecutor.java
@@ -86,10 +86,7 @@ public class GrpcMaterializedViewQueryExecutor implements
MaterializedViewQueryE
BrokerGrpcQueryClient client = getOrCreateClient(broker.getLeft(),
broker.getRight());
- Broker.BrokerRequest brokerRequest = Broker.BrokerRequest.newBuilder()
- .setSql(sql)
- .putAllMetadata(authHeaders)
- .build();
+ Broker.BrokerRequest brokerRequest = buildMaterializationRequest(sql,
authHeaders);
Iterator<Broker.BrokerResponse> responseIterator =
client.submit(brokerRequest);
Preconditions.checkState(responseIterator.hasNext(),
@@ -219,6 +216,30 @@ public class GrpcMaterializedViewQueryExecutor implements
MaterializedViewQueryE
}
}
+ /// Builds the gRPC [Broker.BrokerRequest] for a materialization query,
forcing materialized-view
+ /// rewrite OFF via request **metadata** rather than a SQL `SET`.
+ ///
+ /// The materialization query reads the base table; with broker-wide MV
rewrite enabled it would
+ /// otherwise be eligible to be rewritten back onto an MV over the same base
table (its own MV or
+ /// a sibling), building the MV from MV data instead of the base table.
+ ///
+ /// The disable is carried as request metadata so the broker can apply it as
an override *after*
+ /// parsing (see `BrokerGrpcServer#submit`). A SQL `SET` prefix would be
unreliable: the
+ /// (user-authored) `definedSQL` may already contain an
`enableMaterializedViewRewrite` option,
+ /// and the parser keeps the LAST value for a duplicated key — so a
prepended `SET ... = 'false'`
+ /// would silently lose to a later `SET ... = 'true'` in the query text.
Forcing it from metadata
+ /// at the broker side is unconditional. Safe to force: disabling MV
rewrite only forgoes an
+ /// optimization, never changes results.
+ @VisibleForTesting
+ static Broker.BrokerRequest buildMaterializationRequest(String sql,
Map<String, String> authHeaders) {
+ return Broker.BrokerRequest.newBuilder()
+ .setSql(sql)
+ .putAllMetadata(authHeaders)
+ /// Set AFTER auth headers so the forced flag wins even if an auth
header reused the key.
+
.putMetadata(CommonConstants.Broker.Request.QueryOptionKey.ENABLE_MATERIALIZED_VIEW_REWRITE,
"false")
+ .build();
+ }
+
/// Discovers all gRPC-enabled brokers from Helix and selects one using
round-robin.
/// Also evicts cached clients for brokers that are no longer present.
@VisibleForTesting
diff --git
a/pinot-materialized-view/src/test/java/org/apache/pinot/materializedview/executor/GrpcMaterializedViewQueryExecutorTest.java
b/pinot-materialized-view/src/test/java/org/apache/pinot/materializedview/executor/GrpcMaterializedViewQueryExecutorTest.java
index 74dbbe10577..e0b010dbdff 100644
---
a/pinot-materialized-view/src/test/java/org/apache/pinot/materializedview/executor/GrpcMaterializedViewQueryExecutorTest.java
+++
b/pinot-materialized-view/src/test/java/org/apache/pinot/materializedview/executor/GrpcMaterializedViewQueryExecutorTest.java
@@ -22,11 +22,13 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.helix.HelixManager;
import org.apache.helix.model.InstanceConfig;
import org.apache.pinot.common.config.GrpcConfig;
+import org.apache.pinot.common.proto.Broker;
import org.apache.pinot.spi.utils.CommonConstants;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
@@ -179,6 +181,28 @@ public class GrpcMaterializedViewQueryExecutorTest {
assertEquals(_queryExecutor.getCachedClientCount(), 0);
}
+ @Test
+ public void testBuildMaterializationRequestForcesRewriteOffViaMetadata() {
+ String baseSql = "SELECT a, SUM(b) FROM t WHERE ts >= 1 AND ts < 2 GROUP
BY a LIMIT 10";
+ Map<String, String> authHeaders = Map.of("Authorization", "Bearer token");
+
+ Broker.BrokerRequest request =
+ GrpcMaterializedViewQueryExecutor.buildMaterializationRequest(baseSql,
authHeaders);
+
+ /// The materialization SQL is sent verbatim — no SET injection that an
option already present in
+ /// the user-authored definedSQL could override.
+ assertEquals(request.getSql(), baseSql, "Materialization SQL must be sent
verbatim");
+
+ /// The disable travels as request metadata; the broker force-applies it
after parsing, so it
+ /// cannot be defeated by an `enableMaterializedViewRewrite` option
present in the query text.
+ assertEquals(
+
request.getMetadataMap().get(CommonConstants.Broker.Request.QueryOptionKey.ENABLE_MATERIALIZED_VIEW_REWRITE),
+ "false", "Materialization request must force
enableMaterializedViewRewrite=false via metadata");
+
+ /// Auth headers are preserved alongside the forced flag.
+ assertEquals(request.getMetadataMap().get("Authorization"), "Bearer
token", "Auth headers must be preserved");
+ }
+
private InstanceConfig buildBrokerConfig(String instanceName, String
hostname, int grpcPort) {
InstanceConfig config = new InstanceConfig(instanceName);
config.setHostName(hostname);
diff --git
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
index 6b4628c1ac1..17084994f6d 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
@@ -701,6 +701,16 @@ public class CommonConstants {
* Not a user-facing option.
*/
public static final String MATERIALIZED_VIEW_REWRITE =
"materializedViewRewrite";
+ /// User-facing per-query switch. Default `true`. When set to `false`,
the broker
+ /// skips all materialized-view rewrite for this query and routes to
the base table. Safe to
+ /// expose: disabling only forgoes an optimization and never changes
results. Distinct from
+ /// the broker-internal [#MATERIALIZED_VIEW_REWRITE] marker (which is
stripped from
+ /// user input); this option is honored from user input. The MV minion
executor additionally
+ /// forces it to `false` for its materialization query by passing it
as gRPC request
+ /// metadata, which the broker applies as an override after parsing
(see BrokerGrpcServer) —
+ /// so the materialization query is never rewritten back onto an MV
even if its (user-authored)
+ /// SQL sets this option.
+ public static final String ENABLE_MATERIALIZED_VIEW_REWRITE =
"enableMaterializedViewRewrite";
public static final String EXTRA_PASSIVE_TIMEOUT_MS =
"extraPassiveTimeoutMs";
public static final String SKIP_UPSERT = "skipUpsert";
public static final String SKIP_UPSERT_VIEW = "skipUpsertView";
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]