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 5e7134d193c [MaterializedView] Add scalar function support to
AggregationSubsumptionStrategy (#18681)
5e7134d193c is described below
commit 5e7134d193ca242c3c3ee0e8c8e05c74d1ed826b
Author: Hongkun Xu <[email protected]>
AuthorDate: Thu Aug 6 14:25:39 2026 +0800
[MaterializedView] Add scalar function support to
AggregationSubsumptionStrategy (#18681)
* Add scalar function support to AggregationSubsumptionStrategy projection
match
Allow scalar GROUP BY expressions to match projected materialized-view
columns while preserving each projected grouping expression as an atomic
key during residual validation.
Previously, a view grouped by UPPER(UniqueCarrier) was considered to expose
the raw UniqueCarrier input. A query filtering UniqueCarrier could therefore
be rewritten against a view that did not contain that column. Reject these
rewrites so they fall back to the base table.
Cover exact scalar matching, alias remapping, hidden-source rejection, and
the end-to-end rewrite and fallback paths.
Signed-off-by: Hongkun Xu <[email protected]>
Co-authored-by: Xiang Fu <[email protected]>
* Reject MV candidates whose GROUP BY keys are not materialized
buildResult rewrites every user GROUP BY key to the MV column holding it.
When the MV groups by a key it never projects, that lookup returned null and
the rewritten query carried `GROUP BY <null identifier>`, failing on the
server instead of falling back to the base table.
Treating a scalar as a direct projection hit newly exposed this path: an MV
such as `SELECT UPPER(city) AS uc, SUM(revenue) AS sum_rev FROM orders GROUP
BY city` now matches at the projection stage, so the unmaterialized `city`
key reached the GROUP BY rewrite. The same hole was already reachable
through a plain identifier key.
Pass the projection map into groupByMatches and require every user GROUP BY
key to be projected by the MV. Whole-table re-aggregation is unaffected
because it remaps no key.
Also address review feedback: assert the plan match type instead of the
exact cost constant in the two residual tests, and fail loudly if the source
aggregation seeding the scalar MV segment ever reaches its row limit.
---------
Signed-off-by: Hongkun Xu <[email protected]>
Co-authored-by: Xiang Fu <[email protected]>
Co-authored-by: Xiang Fu <[email protected]>
---
.../MaterializedViewClusterIntegrationTest.java | 159 ++++++++++++
.../strategy/AbstractSubsumptionStrategy.java | 18 +-
.../strategy/AggregationSubsumptionStrategy.java | 92 +++++--
.../rewrite/strategy/ExactSubsumptionStrategy.java | 7 +-
.../rewrite/strategy/ScanSubsumptionStrategy.java | 5 +-
.../AggregationSubsumptionStrategyTest.java | 285 +++++++++++++++++++++
6 files changed, 536 insertions(+), 30 deletions(-)
diff --git
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MaterializedViewClusterIntegrationTest.java
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MaterializedViewClusterIntegrationTest.java
index e15316d293f..99f84cb8bc3 100644
---
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MaterializedViewClusterIntegrationTest.java
+++
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/MaterializedViewClusterIntegrationTest.java
@@ -87,6 +87,10 @@ import static org.testng.Assert.*;
/// ORDER BY + LIMIT + OFFSET; verify correct pagination and ordering.
/// 3. [#testMultipleMaterializedViewCostSelection()] — two MVs match the same
query at
/// different costs; verify the lower-cost MV is selected.
+/// 4. [#testScalarGroupingFunctionRewrite()] and
+/// [#testScalarGroupingFunctionResidualOnSourceColumnFallsBack()] — a
finer-grained MV
+/// groups by a scalar expression; verify both re-aggregation and safe
fallback when a
+/// residual references the unprojected source column.
///
/// **Why this extends [BaseClusterIntegrationTest] directly instead of
///
[org.apache.pinot.integration.tests.custom.CustomDataQueryClusterIntegrationTest]
:**
@@ -105,12 +109,15 @@ public class MaterializedViewClusterIntegrationTest
extends BaseClusterIntegrati
private static final String MATERIALIZED_VIEW_COLD_TABLE_NAME =
"materializedViewColdTable";
private static final String MATERIALIZED_VIEW_SCAN_TABLE_NAME =
"materializedViewScanTable";
private static final String MATERIALIZED_VIEW_COST_TABLE_NAME =
"materializedViewCostTable";
+ private static final String MATERIALIZED_VIEW_SCALAR_TABLE_NAME =
"materializedViewScalarTable";
private static final String MATERIALIZED_VIEW_FULL_TABLE_OFFLINE =
MATERIALIZED_VIEW_FULL_TABLE_NAME + "_OFFLINE";
private static final String MATERIALIZED_VIEW_SPLIT_TABLE_OFFLINE =
MATERIALIZED_VIEW_SPLIT_TABLE_NAME + "_OFFLINE";
private static final String MATERIALIZED_VIEW_COLD_TABLE_OFFLINE =
MATERIALIZED_VIEW_COLD_TABLE_NAME + "_OFFLINE";
private static final String MATERIALIZED_VIEW_SCAN_TABLE_OFFLINE =
MATERIALIZED_VIEW_SCAN_TABLE_NAME + "_OFFLINE";
private static final String MATERIALIZED_VIEW_COST_TABLE_OFFLINE =
MATERIALIZED_VIEW_COST_TABLE_NAME + "_OFFLINE";
+ private static final String MATERIALIZED_VIEW_SCALAR_TABLE_OFFLINE =
+ MATERIALIZED_VIEW_SCALAR_TABLE_NAME + "_OFFLINE";
private static final String TIME_COLUMN = "DaysSinceEpoch";
@@ -125,6 +132,12 @@ public class MaterializedViewClusterIntegrationTest
extends BaseClusterIntegrati
private static final String[] CARRIERS = {"AA", "DL", "UA", "WN", "US",
"B6", "OO", "EV", "MQ", "NK"};
+ /// Row cap for the source aggregation that seeds an MV segment. The MV
must hold *every* group
+ /// or the row-for-row comparison against the rewrite-disabled baseline
would validate a
+ /// truncated view; callers assert the returned row count stays strictly
below this cap so a
+ /// dataset that outgrows it fails loudly instead of silently materializing
a prefix.
+ private static final int MATERIALIZED_VIEW_SOURCE_ROW_LIMIT = 10000;
+
private PinotHelixResourceManager _helixResourceManager;
private HelixPropertyStore<ZNRecord> _propertyStore;
@@ -205,6 +218,7 @@ public class MaterializedViewClusterIntegrationTest extends
BaseClusterIntegrati
setupColdStartMv();
setupScanMv();
setupCostCompetitorMv();
+ setupScalarGroupingMv();
/// Wait for the broker's MaterializedViewMetadataCache to register every
newly-published MV
/// by polling a sentinel query that should be served by the full-rewrite
MV. Polling on a
@@ -213,6 +227,9 @@ public class MaterializedViewClusterIntegrationTest extends
BaseClusterIntegrati
/// race the timeout.
waitForMaterializedViewRegistered(MATERIALIZED_VIEW_FULL_TABLE_OFFLINE,
"SELECT Carrier, SUM(ArrDelayMinutes) FROM " + SOURCE_TABLE_NAME + "
GROUP BY Carrier");
+ waitForMaterializedViewRegistered(MATERIALIZED_VIEW_SCALAR_TABLE_OFFLINE,
+ "SELECT UPPER(UniqueCarrier), SUM(ArrDelayMinutes) FROM " +
SOURCE_TABLE_NAME
+ + " GROUP BY UPPER(UniqueCarrier)");
}
/// Polls a query that is known to be rewritable to the given MV until the
broker's metadata
@@ -243,6 +260,7 @@ public class MaterializedViewClusterIntegrationTest extends
BaseClusterIntegrati
cleanupMaterializedViewMetadata(MATERIALIZED_VIEW_COLD_TABLE_OFFLINE);
cleanupMaterializedViewMetadata(MATERIALIZED_VIEW_SCAN_TABLE_OFFLINE);
cleanupMaterializedViewMetadata(MATERIALIZED_VIEW_COST_TABLE_OFFLINE);
+ cleanupMaterializedViewMetadata(MATERIALIZED_VIEW_SCALAR_TABLE_OFFLINE);
/// Drop MV tables before the base table so the controller's
referential-integrity check
/// (introduced in pinot-controller to prevent orphaned MVs) does not
block the base drop.
@@ -251,6 +269,7 @@ public class MaterializedViewClusterIntegrationTest extends
BaseClusterIntegrati
dropOfflineTable(MATERIALIZED_VIEW_COLD_TABLE_NAME);
dropOfflineTable(MATERIALIZED_VIEW_SCAN_TABLE_NAME);
dropOfflineTable(MATERIALIZED_VIEW_COST_TABLE_NAME);
+ dropOfflineTable(MATERIALIZED_VIEW_SCALAR_TABLE_NAME);
dropOfflineTable(SOURCE_TABLE_NAME);
stopServer();
@@ -834,6 +853,79 @@ public class MaterializedViewClusterIntegrationTest
extends BaseClusterIntegrati
"Full MV (EXACT, cost 0.0) should be selected over cost competitor
(AGG_REAGG, cost 6.0)");
}
+ /// -----------------------------------------------------------------------
+ /// Phase 3, Test 10: Scalar grouping function with re-aggregation
+ /// -----------------------------------------------------------------------
+
+ /// Verifies that `AggregationSubsumptionStrategy` can match a scalar
grouping
+ /// expression. The MV is finer grained (`UPPER(UniqueCarrier), Origin`)
than the user
+ /// query (`UPPER(UniqueCarrier)`), so the MV aggregate must be
re-aggregated.
+ @Test
+ public void testScalarGroupingFunctionRewrite()
+ throws Exception {
+ String query = ""
+ + "SELECT UPPER(UniqueCarrier) AS upper_UniqueCarrier, "
+ + "SUM(ArrDelayMinutes) AS sum_ArrDelayMinutes "
+ + "FROM " + SOURCE_TABLE_NAME + " "
+ + "GROUP BY UPPER(UniqueCarrier) "
+ + "ORDER BY UPPER(UniqueCarrier) "
+ + "LIMIT 10000";
+
+ JsonNode materializedViewResponse = postQuery(query);
+ assertNoExceptions(materializedViewResponse);
+ assertEquals(getMaterializedViewQueried(materializedViewResponse),
MATERIALIZED_VIEW_SCALAR_TABLE_OFFLINE,
+ "Expected the finer-grained scalar grouping MV to be selected");
+
+ JsonNode directResponse = postQuery("SET
enableMaterializedViewRewrite='false'; " + query);
+ assertNoExceptions(directResponse);
+ assertNull(getMaterializedViewQueried(directResponse),
+ "Rewrite-disabled baseline must query the base table");
+
+ JsonNode materializedViewResult =
materializedViewResponse.get("resultTable");
+ JsonNode directResult = directResponse.get("resultTable");
+ assertNotNull(materializedViewResult);
+ assertNotNull(directResult);
+ assertTrue(materializedViewResult.get("rows").size() > 0, "Result should
have rows");
+ assertEquals(materializedViewResult.get("dataSchema"),
directResult.get("dataSchema"),
+ "MV rewrite should preserve the complete result schema");
+ assertEquals(materializedViewResult.get("rows"), directResult.get("rows"),
+ "MV rewrite should return the same complete, ordered result as the
base table");
+ }
+
+ /// Verifies that a residual on the raw source column hidden inside a scalar
grouping
+ /// expression rejects the MV and safely falls back to the base table.
`UniqueCarrier` is not
+ /// projected by any other MV in this test, so an MV hit would expose the
invalid rewrite.
+ @Test
+ public void testScalarGroupingFunctionResidualOnSourceColumnFallsBack()
+ throws Exception {
+ String query = ""
+ + "SELECT UPPER(UniqueCarrier) AS upper_UniqueCarrier, "
+ + "SUM(ArrDelayMinutes) AS sum_ArrDelayMinutes "
+ + "FROM " + SOURCE_TABLE_NAME + " "
+ + "WHERE UniqueCarrier = 'AA' "
+ + "GROUP BY UPPER(UniqueCarrier) "
+ + "ORDER BY UPPER(UniqueCarrier) "
+ + "LIMIT 10000";
+
+ JsonNode fallbackResponse = postQuery(query);
+ assertNoExceptions(fallbackResponse);
+ assertNull(getMaterializedViewQueried(fallbackResponse),
+ "A residual on the unprojected raw source column must reject the
scalar-grouping MV");
+
+ JsonNode directResponse = postQuery("SET
enableMaterializedViewRewrite='false'; " + query);
+ assertNoExceptions(directResponse);
+
+ JsonNode fallbackResult = fallbackResponse.get("resultTable");
+ JsonNode directResult = directResponse.get("resultTable");
+ assertNotNull(fallbackResult);
+ assertNotNull(directResult);
+ assertTrue(fallbackResult.get("rows").size() > 0, "Fallback result should
have rows");
+ assertEquals(fallbackResult.get("dataSchema"),
directResult.get("dataSchema"),
+ "Fallback should preserve the complete result schema");
+ assertEquals(fallbackResult.get("rows"), directResult.get("rows"),
+ "Fallback should return the same complete, ordered result as the
rewrite-disabled query");
+ }
+
/// -----------------------------------------------------------------------
/// MV table setup
/// -----------------------------------------------------------------------
@@ -1107,6 +1199,73 @@ public class MaterializedViewClusterIntegrationTest
extends BaseClusterIntegrati
MaterializedViewRuntimeMetadataUtils.persist(_propertyStore, runtime, -1);
}
+ /// Scalar-grouping MV: groups by `UPPER(UniqueCarrier), Origin` with full
coverage.
+ /// The extra `Origin` key makes it finer grained than the query exercised by
+ /// [#testScalarGroupingFunctionRewrite()], forcing aggregate re-aggregation.
+ private void setupScalarGroupingMv()
+ throws Exception {
+ Schema materializedViewSchema = new Schema.SchemaBuilder()
+ .setSchemaName(MATERIALIZED_VIEW_SCALAR_TABLE_NAME)
+ .addSingleValueDimension("upper_UniqueCarrier",
FieldSpec.DataType.STRING)
+ .addSingleValueDimension("Origin", FieldSpec.DataType.STRING)
+ .addMetric("sum_ArrDelayMinutes", FieldSpec.DataType.DOUBLE)
+ .build();
+ addSchema(materializedViewSchema);
+
+ TableConfig materializedViewTableConfig = new
TableConfigBuilder(TableType.OFFLINE)
+ .setTableName(MATERIALIZED_VIEW_SCALAR_TABLE_NAME)
+ .setNumReplicas(1)
+ .build();
+ addTableConfig(materializedViewTableConfig);
+
+ /// Materialize the exact source-table aggregates so the rewrite can be
checked
+ /// row-for-row against a rewrite-disabled baseline rather than synthetic
values.
+ String sourceAggregationQuery = ""
+ + "SET enableMaterializedViewRewrite='false'; "
+ + "SELECT UPPER(UniqueCarrier), Origin, SUM(ArrDelayMinutes) "
+ + "FROM " + SOURCE_TABLE_NAME + " "
+ + "GROUP BY UPPER(UniqueCarrier), Origin "
+ + "ORDER BY UPPER(UniqueCarrier), Origin "
+ + "LIMIT " + MATERIALIZED_VIEW_SOURCE_ROW_LIMIT;
+ JsonNode sourceAggregationResponse = postQuery(sourceAggregationQuery);
+ assertNoExceptions(sourceAggregationResponse);
+
+ JsonNode sourceRows =
sourceAggregationResponse.get("resultTable").get("rows");
+ assertTrue(sourceRows.size() > 0, "Source aggregation should produce
rows");
+ assertTrue(sourceRows.size() < MATERIALIZED_VIEW_SOURCE_ROW_LIMIT,
+ "Source aggregation hit the row limit, so the MV would hold only a
prefix of the groups "
+ + "and the row-for-row comparison would validate a truncated view.
Raise "
+ + "MATERIALIZED_VIEW_SOURCE_ROW_LIMIT.");
+ List<GenericRow> rows = new ArrayList<>(sourceRows.size());
+ for (JsonNode sourceRow : sourceRows) {
+ GenericRow row = new GenericRow();
+ row.putValue("upper_UniqueCarrier", sourceRow.get(0).asText());
+ row.putValue("Origin", sourceRow.get(1).asText());
+ row.putValue("sum_ArrDelayMinutes", sourceRow.get(2).asDouble());
+ rows.add(row);
+ }
+ buildAndUploadSegment(materializedViewTableConfig, materializedViewSchema,
rows,
+ MATERIALIZED_VIEW_SCALAR_TABLE_NAME, "materializedViewScalarSeg");
+
+ waitForAnyDocLoaded(MATERIALIZED_VIEW_SCALAR_TABLE_NAME, 60_000L);
+
+ String definedSql = "SELECT UPPER(UniqueCarrier) AS upper_UniqueCarrier,
Origin, "
+ + "SUM(ArrDelayMinutes) AS sum_ArrDelayMinutes "
+ + "FROM " + SOURCE_TABLE_NAME + " "
+ + "GROUP BY UPPER(UniqueCarrier), Origin";
+ MaterializedViewDefinitionMetadata definition = new
MaterializedViewDefinitionMetadata(
+ MATERIALIZED_VIEW_SCALAR_TABLE_OFFLINE,
+ List.of(SOURCE_TABLE_NAME),
+ definedSql,
+ Map.of(),
+ null);
+ MaterializedViewDefinitionMetadataUtils.persist(_propertyStore,
definition, -1);
+
+ MaterializedViewRuntimeMetadata runtime = new
MaterializedViewRuntimeMetadata(
+ MATERIALIZED_VIEW_SCALAR_TABLE_OFFLINE, DATA_MAX_TIME_MS, new
HashMap<>());
+ MaterializedViewRuntimeMetadataUtils.persist(_propertyStore, runtime, -1);
+ }
+
/// -----------------------------------------------------------------------
/// Segment building helpers
/// -----------------------------------------------------------------------
diff --git
a/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/AbstractSubsumptionStrategy.java
b/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/AbstractSubsumptionStrategy.java
index debc829f4f0..e9eacaf9bdf 100644
---
a/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/AbstractSubsumptionStrategy.java
+++
b/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/AbstractSubsumptionStrategy.java
@@ -91,14 +91,17 @@ public abstract class AbstractSubsumptionStrategy
implements MaterializedViewMat
return null;
}
+ /// The projection map cached on the entry is needed from Step 2 onward: a
GROUP BY key is only
+ /// usable if the MV actually materialized a column for it.
+ Map<Expression, String> viewProjectionMap =
candidateEntry.getViewProjectionMap();
+
/// Step 2: GROUP BY
- if (!groupByMatches(userQuery, viewQuery)) {
+ if (!groupByMatches(userQuery, viewQuery, viewProjectionMap)) {
LOGGER.debug("MV match [{}] strategy={}: rejected at GROUP_BY",
materializedViewName, strategyName);
return null;
}
- /// Step 3: projection subsumption — use the projection map cached on the
entry.
- Map<Expression, String> viewProjectionMap =
candidateEntry.getViewProjectionMap();
+ /// Step 3: projection subsumption
if (!projectionSubsumes(userQuery.getSelectList(), viewProjectionMap)) {
LOGGER.debug("MV match [{}] strategy={}: rejected at PROJECTION",
materializedViewName, strategyName);
return null;
@@ -171,7 +174,14 @@ public abstract class AbstractSubsumptionStrategy
implements MaterializedViewMat
/// Returns `true` if the user query's GROUP BY clause is compatible
/// with the MV's GROUP BY clause.
- protected abstract boolean groupByMatches(PinotQuery userQuery, PinotQuery
viewQuery);
+ ///
+ /// @param userQuery the user's compiled query
+ /// @param viewQuery the MV's compiled query
+ /// @param viewProjectionMap alias-stripped expression → MV column
name. Strategies that
+ /// rewrite the user's GROUP BY to MV columns must
use this to reject
+ /// keys the MV groups by but does not materialize
a column for.
+ protected abstract boolean groupByMatches(PinotQuery userQuery, PinotQuery
viewQuery,
+ Map<Expression, String> viewProjectionMap);
/// Returns `true` if the MV's projection (SELECT list) covers all
/// expressions required by the user query.
diff --git
a/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/AggregationSubsumptionStrategy.java
b/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/AggregationSubsumptionStrategy.java
index 1fc786d1671..55960a910f3 100644
---
a/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/AggregationSubsumptionStrategy.java
+++
b/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/AggregationSubsumptionStrategy.java
@@ -69,7 +69,8 @@ public class AggregationSubsumptionStrategy extends
AbstractSubsumptionStrategy
}
@Override
- protected boolean groupByMatches(PinotQuery userQuery, PinotQuery viewQuery)
{
+ protected boolean groupByMatches(PinotQuery userQuery, PinotQuery viewQuery,
+ Map<Expression, String> viewProjectionMap) {
List<Expression> userGroupBy = userQuery.getGroupByList();
List<Expression> materializedViewGroupBy = viewQuery.getGroupByList();
@@ -90,7 +91,22 @@ public class AggregationSubsumptionStrategy extends
AbstractSubsumptionStrategy
Set<Expression> userSet = new HashSet<>(userGroupBy);
Set<Expression> materializedViewSet = new
HashSet<>(materializedViewGroupBy);
- return materializedViewSet.containsAll(userSet);
+ if (!materializedViewSet.containsAll(userSet)) {
+ return false;
+ }
+
+ /// The MV grouping by a key is not enough — buildResult rewrites every
user GROUP BY key to the
+ /// MV column that holds it, so the MV must also *project* each key. An
MV such as
+ /// `SELECT UPPER(city) AS uc, SUM(revenue) AS sum_rev FROM orders GROUP
BY city` groups by
+ /// `city` without materializing it: the rewritten query would carry
`GROUP BY <null>` and fail
+ /// on the server instead of falling back to the base table. Reject here
so the user query
+ /// simply runs against the base table.
+ for (Expression userGroupByExpr : userSet) {
+ if (!viewProjectionMap.containsKey(userGroupByExpr)) {
+ return false;
+ }
+ }
+ return true;
}
@Override
@@ -101,19 +117,20 @@ public class AggregationSubsumptionStrategy extends
AbstractSubsumptionStrategy
}
for (Expression expr : userSelectList) {
Expression stripped = MaterializedViewMatchUtils.stripAlias(expr);
- /// Plain column reference: direct MV projection hit is sufficient.
- if (stripped.getFunctionCall() == null) {
+ boolean isAggregate = CalciteSqlParser.isAggregateExpression(stripped);
+ if (isAggregate) {
+ /// Aggregate (incl. ROUND(SUM(x))): rewritable only if a registered
equivalence exists,
+ /// else rejected. No new aggregates supported here.
+ if (findEquivalentMaterializedViewEntry(stripped, viewProjectionMap)
== null) {
+ return false;
+ }
+ } else {
+ /// Plain column OR scalar grouping function (e.g. DATETRUNC('DAY',
ts)): a direct MV
+ /// projection hit is sufficient — the containsKey check below
confirms the MV
+ /// materialized this exact expression.
if (!viewProjectionMap.containsKey(stripped)) {
return false;
}
- continue;
- }
- /// Aggregate function: an exact projection match is NOT enough on its
own. We need an
- /// AggregationEquivalence rule to re-aggregate the pre-computed MV
column correctly.
- /// Without a rule we would fall back to a bare column reference (e.g.
AVG(revenue) →
- /// avg_rev), which produces wrong results for non-distributive
functions.
- if (findEquivalentMaterializedViewEntry(stripped, viewProjectionMap) ==
null) {
- return false;
}
}
return true;
@@ -192,13 +209,34 @@ public class AggregationSubsumptionStrategy extends
AbstractSubsumptionStrategy
return false;
}
- Set<String> groupByColumnNames = new
HashSet<>(materializedViewGroupBy.size());
- for (Expression gbExpr : materializedViewGroupBy) {
-
groupByColumnNames.addAll(MaterializedViewMatchUtils.collectReferencedColumns(gbExpr));
- }
+ return residualReferencesProjectedGroupKeys(residualFilter, new
HashSet<>(materializedViewGroupBy),
+ viewProjectionMap);
+ }
- Set<String> residualColumns =
MaterializedViewMatchUtils.collectReferencedColumns(residualFilter);
- return groupByColumnNames.containsAll(residualColumns);
+ private static boolean residualReferencesProjectedGroupKeys(Expression expr,
+ Set<Expression> materializedViewGroupBy, Map<Expression, String>
viewProjectionMap) {
+ /// Treat a projected GROUP BY expression as an atomic value. For example,
+ /// DATETRUNC('DAY', ts) can be remapped to the MV's `day` column, but its
source column `ts`
+ /// is not available at the original granularity and cannot be used in a
bare residual filter.
+ if (materializedViewGroupBy.contains(expr) &&
viewProjectionMap.containsKey(expr)) {
+ return true;
+ }
+ if (expr.getType() == ExpressionType.LITERAL) {
+ return true;
+ }
+ if (expr.getType() == ExpressionType.IDENTIFIER) {
+ return false;
+ }
+ Function function = expr.getFunctionCall();
+ if (function == null || function.getOperands() == null) {
+ return false;
+ }
+ for (Expression operand : function.getOperands()) {
+ if (!residualReferencesProjectedGroupKeys(operand,
materializedViewGroupBy, viewProjectionMap)) {
+ return false;
+ }
+ }
+ return true;
}
@Override
@@ -280,6 +318,8 @@ public class AggregationSubsumptionStrategy extends
AbstractSubsumptionStrategy
if (userGroupBy != null && !userGroupBy.isEmpty()) {
List<Expression> remappedGroupBy = new ArrayList<>(userGroupBy.size());
for (Expression gbExpr : userGroupBy) {
+ /// Never null: groupByMatches rejected the candidate unless every
user GROUP BY key is
+ /// projected by the MV. A null here would silently emit `GROUP BY
<null identifier>`.
String materializedViewCol = viewProjectionMap.get(gbExpr);
remappedGroupBy.add(RequestUtils.getIdentifierExpression(materializedViewCol));
}
@@ -346,11 +386,15 @@ public class AggregationSubsumptionStrategy extends
AbstractSubsumptionStrategy
String userAlias = MaterializedViewMatchUtils.extractUserAlias(expr);
Expression rewritten;
- if (viewProjectionMap.containsKey(stripped)
- && stripped.getFunctionCall() == null) {
+ boolean isAggregate = CalciteSqlParser.isAggregateExpression(stripped);
+ if (!isAggregate && viewProjectionMap.containsKey(stripped)) {
+ /// Plain column OR scalar grouping function: project the MV column
directly.
rewritten =
RequestUtils.getIdentifierExpression(viewProjectionMap.get(stripped));
- } else {
+ } else if (isAggregate) {
rewritten = rewriteAggregationExpression(stripped, viewProjectionMap);
+ } else {
+ throw new IllegalStateException(
+ "Cannot rewrite non-aggregate expression without MV projection: "
+ stripped);
}
/// Preserve the user's expected result-column name. Bare-identifier
dimensions that map 1:1
@@ -391,11 +435,13 @@ public class AggregationSubsumptionStrategy extends
AbstractSubsumptionStrategy
private Expression remapExpressionWithEquivalence(Expression expr,
Map<Expression, String> viewProjectionMap) {
- if (viewProjectionMap.containsKey(expr) && expr.getFunctionCall() == null)
{
+ /// Plain column OR scalar grouping function with a direct MV hit: map to
the MV column.
+ boolean isAggregate = CalciteSqlParser.isAggregateExpression(expr);
+ if (!isAggregate && viewProjectionMap.containsKey(expr)) {
return RequestUtils.getIdentifierExpression(viewProjectionMap.get(expr));
}
- if (expr.getFunctionCall() != null) {
+ if (isAggregate && expr.getFunctionCall() != null) {
if (viewProjectionMap.containsKey(expr)) {
return rewriteAggregationExpression(expr, viewProjectionMap);
}
diff --git
a/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/ExactSubsumptionStrategy.java
b/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/ExactSubsumptionStrategy.java
index 6264556996d..896b4e06cb0 100644
---
a/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/ExactSubsumptionStrategy.java
+++
b/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/ExactSubsumptionStrategy.java
@@ -63,9 +63,12 @@ public class ExactSubsumptionStrategy extends
AbstractSubsumptionStrategy {
return true;
}
- /// Requires GROUP BY lists to be identical (same expressions, same order).
+ /// Requires GROUP BY lists to be identical (same expressions, same order).
The projection map
+ /// is not consulted: [#projectionSubsumes] already requires the SELECT
lists to be equal, so an
+ /// identical GROUP BY key is necessarily materialized.
@Override
- protected boolean groupByMatches(PinotQuery userQuery, PinotQuery viewQuery)
{
+ protected boolean groupByMatches(PinotQuery userQuery, PinotQuery viewQuery,
+ Map<Expression, String> viewProjectionMap) {
List<Expression> userList = userQuery.getGroupByList();
List<Expression> materializedViewList = viewQuery.getGroupByList();
if (userList == null && materializedViewList == null) {
diff --git
a/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/ScanSubsumptionStrategy.java
b/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/ScanSubsumptionStrategy.java
index e091b7fe068..b94dd815154 100644
---
a/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/ScanSubsumptionStrategy.java
+++
b/pinot-materialized-view/src/main/java/org/apache/pinot/materializedview/rewrite/strategy/ScanSubsumptionStrategy.java
@@ -55,8 +55,11 @@ public class ScanSubsumptionStrategy extends
AbstractSubsumptionStrategy {
&& MaterializedViewQueryShape.classify(viewQuery) ==
MaterializedViewQueryShape.SCAN;
}
+ /// Scan queries have no GROUP BY on either side, so there is no key to
remap and the projection
+ /// map is not consulted.
@Override
- protected boolean groupByMatches(PinotQuery userQuery, PinotQuery viewQuery)
{
+ protected boolean groupByMatches(PinotQuery userQuery, PinotQuery viewQuery,
+ Map<Expression, String> viewProjectionMap) {
return !userQuery.isSetGroupByList() && !viewQuery.isSetGroupByList();
}
diff --git
a/pinot-materialized-view/src/test/java/org/apache/pinot/materializedview/rewrite/AggregationSubsumptionStrategyTest.java
b/pinot-materialized-view/src/test/java/org/apache/pinot/materializedview/rewrite/AggregationSubsumptionStrategyTest.java
index a902f0ffce6..e20a511a126 100644
---
a/pinot-materialized-view/src/test/java/org/apache/pinot/materializedview/rewrite/AggregationSubsumptionStrategyTest.java
+++
b/pinot-materialized-view/src/test/java/org/apache/pinot/materializedview/rewrite/AggregationSubsumptionStrategyTest.java
@@ -892,4 +892,289 @@ public class AggregationSubsumptionStrategyTest {
.getFunctionCall().getOperands().get(0).getIdentifier().getName(),
"raw_hll_FlightNum");
assertTrue(rewritten.getOrderByList().get(0).toString().contains("raw_hll_FlightNum"));
}
+
+ /// =======================================================================
+ /// Scalar grouping function support (e.g. DATETRUNC)
+ /// =======================================================================
+
+ /// A scalar grouping function (DATETRUNC) in the SELECT list must be
treated as a plain
+ /// projection — a direct MV column hit is sufficient. It must NOT be routed
through the
+ /// aggregation-equivalence path (which would reject it for lack of a
re-aggregation rule).
+ @Test
+ public void testScalarGroupingFunctionExactMatch() {
+ String definedSql =
+ "SELECT DATETRUNC('DAY', ts) AS day, SUM(revenue) AS sum_rev FROM
orders "
+ + "GROUP BY DATETRUNC('DAY', ts)";
+ MaterializedViewCacheEntry entry = createEntry("mv_orders_OFFLINE",
"orders", definedSql);
+
+ PinotQuery userQuery = CalciteSqlParser.compileToPinotQuery(
+ "SELECT DATETRUNC('DAY', ts), SUM(revenue) FROM orders GROUP BY
DATETRUNC('DAY', ts)");
+ MaterializedViewRewritePlan result = _strategy.match(userQuery, entry);
+
+ assertNotNull(result, "Scalar grouping function should match via direct MV
projection");
+
+ PinotQuery rewritten = result.getMaterializedViewQuery();
+
+ /// GROUP BY remapped to the MV column.
+ assertNotNull(rewritten.getGroupByList());
+ assertEquals(rewritten.getGroupByList().size(), 1);
+ assertEquals(rewritten.getGroupByList().get(0).getIdentifier().getName(),
"day");
+
+ List<Expression> selectList = rewritten.getSelectList();
+ assertEquals(selectList.size(), 2);
+
+ /// DATETRUNC('DAY', ts) → day AS datetrunc('DAY', ts) (direct projection,
alias preserves name).
+ Function dayAlias = selectList.get(0).getFunctionCall();
+ assertNotNull(dayAlias);
+ assertEquals(dayAlias.getOperator(), "as");
+ assertEquals(dayAlias.getOperands().get(0).getIdentifier().getName(),
"day");
+
+ /// SUM(revenue) → SUM(sum_rev) AS sum(revenue).
+ Function sumAlias = selectList.get(1).getFunctionCall();
+ assertNotNull(sumAlias);
+ assertEquals(sumAlias.getOperator(), "as");
+ Function rewrittenSum = sumAlias.getOperands().get(0).getFunctionCall();
+ assertNotNull(rewrittenSum);
+ assertEquals(rewrittenSum.getOperator(), "sum");
+ assertEquals(rewrittenSum.getOperands().get(0).getIdentifier().getName(),
"sum_rev");
+ }
+
+ /// A scalar grouping function used in ORDER BY must remap to the MV column.
+ @Test
+ public void testScalarGroupingFunctionInOrderBy() {
+ String definedSql =
+ "SELECT DATETRUNC('DAY', ts) AS day, SUM(revenue) AS sum_rev FROM
orders "
+ + "GROUP BY DATETRUNC('DAY', ts)";
+ MaterializedViewCacheEntry entry = createEntry("mv_orders_OFFLINE",
"orders", definedSql);
+
+ PinotQuery userQuery = CalciteSqlParser.compileToPinotQuery(
+ "SELECT DATETRUNC('DAY', ts), SUM(revenue) FROM orders "
+ + "GROUP BY DATETRUNC('DAY', ts) ORDER BY DATETRUNC('DAY', ts)
DESC");
+ MaterializedViewRewritePlan result = _strategy.match(userQuery, entry);
+
+ assertNotNull(result);
+ PinotQuery rewritten = result.getMaterializedViewQuery();
+ assertNotNull(rewritten.getOrderByList());
+ assertEquals(rewritten.getOrderByList().size(), 1);
+ Function orderByFunction =
rewritten.getOrderByList().get(0).getFunctionCall();
+ assertNotNull(orderByFunction);
+ assertEquals(orderByFunction.getOperator(), "desc");
+
assertEquals(orderByFunction.getOperands().get(0).getIdentifier().getName(),
"day");
+ }
+
+ /// A scalar grouping function used in HAVING must remap to the MV column.
+ @Test
+ public void testScalarGroupingFunctionInHaving() {
+ String definedSql =
+ "SELECT DATETRUNC('DAY', ts) AS day, SUM(revenue) AS sum_rev FROM
orders "
+ + "GROUP BY DATETRUNC('DAY', ts)";
+ MaterializedViewCacheEntry entry = createEntry("mv_orders_OFFLINE",
"orders", definedSql);
+
+ PinotQuery userQuery = CalciteSqlParser.compileToPinotQuery(
+ "SELECT DATETRUNC('DAY', ts), SUM(revenue) FROM orders "
+ + "GROUP BY DATETRUNC('DAY', ts) HAVING DATETRUNC('DAY', ts) > 0");
+ MaterializedViewRewritePlan result = _strategy.match(userQuery, entry);
+
+ assertNotNull(result);
+ PinotQuery rewritten = result.getMaterializedViewQuery();
+ assertNotNull(rewritten.getHavingExpression());
+ Function havingFunction =
rewritten.getHavingExpression().getFunctionCall();
+ assertNotNull(havingFunction);
+ assertEquals(havingFunction.getOperator(), "GREATER_THAN");
+
assertEquals(havingFunction.getOperands().get(0).getIdentifier().getName(),
"day");
+ }
+
+ /// A scalar function not present in the MV projection (different truncation
unit) must reject.
+ @Test
+ public void testNoMatchScalarFunctionNotMaterialized() {
+ String definedSql =
+ "SELECT DATETRUNC('DAY', ts) AS day, SUM(revenue) AS sum_rev FROM
orders "
+ + "GROUP BY DATETRUNC('DAY', ts)";
+ MaterializedViewCacheEntry entry = createEntry("mv_orders_OFFLINE",
"orders", definedSql);
+
+ PinotQuery userQuery = CalciteSqlParser.compileToPinotQuery(
+ "SELECT DATETRUNC('MONTH', ts), SUM(revenue) FROM orders GROUP BY
DATETRUNC('MONTH', ts)");
+ MaterializedViewRewritePlan result = _strategy.match(userQuery, entry);
+
+ assertNull(result, "DATETRUNC('MONTH', ts) is not a materialized
projection and must be rejected");
+ }
+
+ /// MV is grouped by a superset of the user keys (DATETRUNC('DAY', ts),
city) while the user
+ /// groups only by DATETRUNC('DAY', ts). Re-aggregation is non-trivial here
(multiple MV rows
+ /// collapse per day), and the scalar grouping function must still resolve
as a direct MV hit.
+ @Test
+ public void testScalarGroupingFunctionFinerMaterializedViewGranularity() {
+ String definedSql =
+ "SELECT DATETRUNC('DAY', ts) AS day, city, SUM(revenue) AS sum_rev
FROM orders "
+ + "GROUP BY DATETRUNC('DAY', ts), city";
+ MaterializedViewCacheEntry entry = createEntry("mv_orders_OFFLINE",
"orders", definedSql);
+
+ PinotQuery userQuery = CalciteSqlParser.compileToPinotQuery(
+ "SELECT DATETRUNC('DAY', ts), SUM(revenue) FROM orders GROUP BY
DATETRUNC('DAY', ts)");
+ MaterializedViewRewritePlan result = _strategy.match(userQuery, entry);
+
+ assertNotNull(result, "Finer MV granularity with a scalar grouping key
should re-aggregate");
+
+ PinotQuery rewritten = result.getMaterializedViewQuery();
+ assertNotNull(rewritten.getGroupByList());
+ assertEquals(rewritten.getGroupByList().size(), 1);
+ assertEquals(rewritten.getGroupByList().get(0).getIdentifier().getName(),
"day");
+
+ List<Expression> selectList = rewritten.getSelectList();
+ assertEquals(selectList.size(), 2);
+ Function dayAlias = selectList.get(0).getFunctionCall();
+ assertNotNull(dayAlias);
+ assertEquals(dayAlias.getOperator(), "as");
+ assertEquals(dayAlias.getOperands().get(0).getIdentifier().getName(),
"day");
+
+ Function sumAlias = selectList.get(1).getFunctionCall();
+ assertNotNull(sumAlias);
+ Function rewrittenSum = sumAlias.getOperands().get(0).getFunctionCall();
+ assertNotNull(rewrittenSum);
+ assertEquals(rewrittenSum.getOperands().get(0).getIdentifier().getName(),
"sum_rev");
+ }
+
+ /// The raw source column inside a scalar grouping expression is not
available in the MV.
+ /// Filtering on `ts` after it has been truncated to `day` would either
reference a missing
+ /// column or incorrectly apply an intra-day predicate to a whole-day bucket.
+ @Test
+ public void testScalarGroupingFunctionRejectsResidualOnSourceColumn() {
+ String definedSql =
+ "SELECT DATETRUNC('DAY', ts) AS day, SUM(revenue) AS sum_rev FROM
orders "
+ + "GROUP BY DATETRUNC('DAY', ts)";
+ MaterializedViewCacheEntry entry = createEntry("mv_orders_OFFLINE",
"orders", definedSql);
+
+ PinotQuery userQuery = CalciteSqlParser.compileToPinotQuery(
+ "SELECT DATETRUNC('DAY', ts), SUM(revenue) FROM orders WHERE ts >=
1000 "
+ + "GROUP BY DATETRUNC('DAY', ts)");
+ MaterializedViewRewritePlan result = _strategy.match(userQuery, entry);
+
+ assertNull(result, "Bare source column 'ts' is not projected by the
materialized view");
+ }
+
+ /// The complete scalar grouping expression is available as one atomic MV
column, so a residual
+ /// on that exact expression is safe and must be remapped without exposing
its source column.
+ @Test
+ public void
testScalarGroupingFunctionAllowsResidualOnMaterializedExpression() {
+ String definedSql =
+ "SELECT DATETRUNC('DAY', ts) AS day, SUM(revenue) AS sum_rev FROM
orders "
+ + "GROUP BY DATETRUNC('DAY', ts)";
+ MaterializedViewCacheEntry entry = createEntry("mv_orders_OFFLINE",
"orders", definedSql);
+
+ PinotQuery userQuery = CalciteSqlParser.compileToPinotQuery(
+ "SELECT DATETRUNC('DAY', ts), SUM(revenue) FROM orders "
+ + "WHERE DATETRUNC('DAY', ts) >= 1000 GROUP BY DATETRUNC('DAY',
ts)");
+ MaterializedViewRewritePlan result = _strategy.match(userQuery, entry);
+
+ assertNotNull(result, "The complete materialized grouping expression is a
valid residual");
+ assertEquals(result.getMatchType(), MatchType.AGG_REAGG);
+ Expression residual =
result.getMaterializedViewQuery().getFilterExpression();
+ assertNotNull(residual, "The residual must be retained on the MV side, not
dropped");
+
assertEquals(residual.getFunctionCall().getOperands().get(0).getIdentifier().getName(),
"day");
+ }
+
+ /// A scalar grouping key must not prevent residual filtering on another
grouping key that the
+ /// MV projects directly. The aliased projection also verifies that the
residual is remapped to
+ /// the MV-side column name.
+ @Test
+ public void
testScalarGroupingFunctionAllowsResidualOnProjectedIdentifierGroupKey() {
+ String definedSql =
+ "SELECT DATETRUNC('DAY', ts) AS day, city AS mv_city, SUM(revenue) AS
sum_rev FROM orders "
+ + "GROUP BY DATETRUNC('DAY', ts), city";
+ MaterializedViewCacheEntry entry = createEntry("mv_orders_OFFLINE",
"orders", definedSql);
+
+ PinotQuery userQuery = CalciteSqlParser.compileToPinotQuery(
+ "SELECT DATETRUNC('DAY', ts), SUM(revenue) FROM orders WHERE city =
'NYC' "
+ + "GROUP BY DATETRUNC('DAY', ts)");
+ MaterializedViewRewritePlan result = _strategy.match(userQuery, entry);
+
+ assertNotNull(result, "Directly projected identifier group keys remain
valid residuals");
+ assertEquals(result.getMatchType(), MatchType.AGG_REAGG);
+ Expression residual =
result.getMaterializedViewQuery().getFilterExpression();
+ assertNotNull(residual, "The residual must be retained on the MV side, not
dropped");
+
assertEquals(residual.getFunctionCall().getOperands().get(0).getIdentifier().getName(),
"mv_city");
+ }
+
+ /// Regression: an MV may group by a key it never materializes — here `city`
is only visible
+ /// through `UPPER(city)`. Matching such an MV would rewrite the user's
`GROUP BY city` to the
+ /// missing column and emit `GROUP BY <null identifier>`, failing on the
server instead of
+ /// falling back to the base table. Treating the scalar as a direct
projection hit must not
+ /// bypass that check.
+ @Test
+ public void testNoMatchScalarProjectedButGroupKeyNotMaterialized() {
+ String definedSql =
+ "SELECT UPPER(city) AS uc, SUM(revenue) AS sum_rev FROM orders GROUP
BY city";
+ MaterializedViewCacheEntry entry = createEntry("mv_orders_OFFLINE",
"orders", definedSql);
+
+ PinotQuery userQuery = CalciteSqlParser.compileToPinotQuery(
+ "SELECT UPPER(city), SUM(revenue) FROM orders GROUP BY city");
+ MaterializedViewRewritePlan result = _strategy.match(userQuery, entry);
+
+ assertNull(result, "'city' is grouped by the MV but not materialized as a
column");
+ }
+
+ /// The same invariant for a plain identifier grouping key: an MV that
groups by `city` without
+ /// projecting it cannot answer a query grouped by `city`.
+ @Test
+ public void testNoMatchGroupKeyNotMaterialized() {
+ String definedSql = "SELECT SUM(revenue) AS sum_rev FROM orders GROUP BY
city";
+ MaterializedViewCacheEntry entry = createEntry("mv_orders_OFFLINE",
"orders", definedSql);
+
+ PinotQuery userQuery = CalciteSqlParser.compileToPinotQuery(
+ "SELECT SUM(revenue) FROM orders GROUP BY city");
+ MaterializedViewRewritePlan result = _strategy.match(userQuery, entry);
+
+ assertNull(result, "'city' is grouped by the MV but not materialized as a
column");
+ }
+
+ /// Counterpart to the two rejections above: once the MV also projects the
grouping key, the
+ /// scalar projection resolves and the key remaps to the MV column.
+ @Test
+ public void testScalarProjectedWithMaterializedGroupKey() {
+ String definedSql =
+ "SELECT city, UPPER(city) AS uc, SUM(revenue) AS sum_rev FROM orders
GROUP BY city";
+ MaterializedViewCacheEntry entry = createEntry("mv_orders_OFFLINE",
"orders", definedSql);
+
+ PinotQuery userQuery = CalciteSqlParser.compileToPinotQuery(
+ "SELECT UPPER(city), SUM(revenue) FROM orders GROUP BY city");
+ MaterializedViewRewritePlan result = _strategy.match(userQuery, entry);
+
+ assertNotNull(result, "Both the grouping key and the scalar projection are
materialized");
+ PinotQuery rewritten = result.getMaterializedViewQuery();
+ assertEquals(rewritten.getGroupByList().size(), 1);
+ assertEquals(rewritten.getGroupByList().get(0).getIdentifier().getName(),
"city");
+
+ Function upperAlias = rewritten.getSelectList().get(0).getFunctionCall();
+ assertNotNull(upperAlias);
+ assertEquals(upperAlias.getOperator(), "as");
+ assertEquals(upperAlias.getOperands().get(0).getIdentifier().getName(),
"uc");
+ }
+
+ /// A whole-table user aggregate re-aggregates the MV's per-group rows, so
no GROUP BY key needs
+ /// remapping — the unmaterialized MV grouping key must not reject this
rewrite.
+ @Test
+ public void testWholeTableAggregateOverUnmaterializedGroupKey() {
+ String definedSql = "SELECT SUM(revenue) AS sum_rev FROM orders GROUP BY
city";
+ MaterializedViewCacheEntry entry = createEntry("mv_orders_OFFLINE",
"orders", definedSql);
+
+ PinotQuery userQuery = CalciteSqlParser.compileToPinotQuery("SELECT
SUM(revenue) FROM orders");
+ MaterializedViewRewritePlan result = _strategy.match(userQuery, entry);
+
+ assertNotNull(result, "Whole-table re-aggregation does not reference the
MV grouping key");
+ assertNull(result.getMaterializedViewQuery().getGroupByList());
+ }
+
+ /// Regression: a nested aggregate such as ROUND(SUM(x)) must stay on the
aggregate path. There is
+ /// no re-aggregation rule for ROUND, so it must be rejected — never treated
as a scalar direct hit.
+ @Test
+ public void testNoMatchNestedAggregateRoundSum() {
+ String definedSql = "SELECT city, SUM(revenue) AS sum_rev FROM orders
GROUP BY city";
+ MaterializedViewCacheEntry entry = createEntry("mv_orders_OFFLINE",
"orders", definedSql);
+
+ PinotQuery userQuery = CalciteSqlParser.compileToPinotQuery(
+ "SELECT city, ROUND(SUM(revenue)) FROM orders GROUP BY city");
+ MaterializedViewRewritePlan result = _strategy.match(userQuery, entry);
+
+ assertNull(result, "ROUND(SUM(revenue)) has no re-aggregation rule and
must be rejected");
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]