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


##########
pinot-query-runtime/src/test/resources/queries/MatchRecognize.json:
##########
@@ -0,0 +1,93 @@
+{
+  "match_recognize_v_shape": {
+    "tables": {
+      "ticker": {
+        "schema": [
+          {"name": "symbol", "type": "STRING"},
+          {"name": "seq", "type": "LONG"},
+          {"name": "price", "type": "INT"}
+        ],
+        "inputs": [
+          ["A", 1, 10],
+          ["A", 2, 8],
+          ["A", 3, 6],
+          ["A", 4, 9],
+          ["A", 5, 12],
+          ["A", 6, 7],
+          ["A", 7, 11],
+          ["B", 1, 5],
+          ["B", 2, 3],
+          ["B", 3, 8]
+        ]
+      }
+    },
+    "queries": [
+      {
+        "description": "V-shape detection with an explicit AFTER MATCH SKIP 
PAST LAST ROW: matches never overlap. H2 has no MATCH_RECOGNIZE, so the 
expected rows are hand-computed.",
+        "sql": "SELECT * FROM {ticker} MATCH_RECOGNIZE (PARTITION BY symbol 
ORDER BY seq MEASURES FIRST(DOWN.price) AS start_price, LAST(UP.price) AS 
end_price ONE ROW PER MATCH AFTER MATCH SKIP PAST LAST ROW PATTERN (DOWN+ UP+) 
DEFINE DOWN AS DOWN.price < PREV(DOWN.price), UP AS UP.price > PREV(UP.price)) 
AS mr",
+        "outputs": [
+          ["A", 8, 12],
+          ["A", 7, 11],
+          ["B", 3, 8]
+        ],
+        "ignoreV2Optimizer": true,

Review Comment:
   These `ignoreV2Optimizer`/`ignoreLiteMode` flags make 
`ResourceBasedQueriesTest` `SkipException` out of the v2 and lite paths 
entirely, so the behavior of MATCH_RECOGNIZE under `usePhysicalOptimizer=true` 
is never asserted.
   
   Right now that path isn't a clean rejection: `RelToPRelConverter` has no 
`LogicalMatch` branch, so the query falls through to `throw new 
IllegalStateException("Unexpected relNode type: ...LogicalMatch")` — an obscure 
internal error rather than the actionable "not supported" message the PR is 
careful to produce everywhere else.
   
   Suggest (a) adding a targeted, validator-style rejection for MATCH_RECOGNIZE 
under v2/lite, and (b) a test that *positively asserts* that rejection instead 
of skipping, so a future default-flip of the physical optimizer can't silently 
regress it.



##########
pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/MatchOperator.java:
##########
@@ -0,0 +1,356 @@
+/**
+ * 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.query.runtime.operator;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.apache.pinot.common.datatable.StatMap;
+import org.apache.pinot.common.utils.DataSchema;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.core.data.table.Key;
+import org.apache.pinot.query.planner.plannode.MatchNode;
+import org.apache.pinot.query.planner.plannode.PatternSymbol;
+import org.apache.pinot.query.runtime.blocks.MseBlock;
+import org.apache.pinot.query.runtime.blocks.RowHeapDataBlock;
+import org.apache.pinot.query.runtime.operator.match.MatchExpression;
+import org.apache.pinot.query.runtime.operator.match.MatchLimits;
+import org.apache.pinot.query.runtime.operator.match.MatchTape;
+import org.apache.pinot.query.runtime.operator.match.PartitionMatcher;
+import org.apache.pinot.query.runtime.operator.match.PatternNfa;
+import org.apache.pinot.query.runtime.operator.match.PatternToNfaCompiler;
+import org.apache.pinot.query.runtime.operator.utils.AggregationUtils;
+import org.apache.pinot.query.runtime.operator.utils.TypeUtils;
+import org.apache.pinot.query.runtime.plan.OpChainExecutionContext;
+import org.apache.pinot.spi.exception.QueryErrorCode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/// Evaluates SQL:2016 `MATCH_RECOGNIZE` (row pattern recognition) with `ONE 
ROW PER MATCH`.
+///
+/// ## What it does per partition
+///
+/// For every `PARTITION BY` partition, in `ORDER BY` order, it walks a scan 
position from the first row to
+/// the last. At each position it asks [PartitionMatcher] for the preferred 
match starting exactly there. On a
+/// match it emits one row - the partition key columns followed by the 
`MEASURES` - and then moves the scan
+/// position according to the `AFTER MATCH SKIP` mode. On no match it moves 
one row forward.
+///
+/// ## What it expects from the plan
+///
+/// Like [WindowAggregateOperator], this operator does not sort. 
`PinotMatchExchangeNodeInsertRule` puts a
+/// sort exchange underneath that hash distributes on the partition keys and 
sorts the receiver side on
+/// `(partitionKeys..., orderKeys...)`, so rows arrive grouped by partition 
and ordered within a partition. The
+/// operator therefore buffers one partition at a time and releases it at each 
boundary, and never reads
+/// [MatchNode#getCollations()]: the ordering has already been established 
below it.
+///
+/// The grouping half of that assumption is verified rather than trusted: if a 
partition key reappears after its
+/// partition was closed, the operator fails instead of silently splitting one 
partition into two and reporting matches
+/// that do not exist. The ordering half is not re-checked per row, because an 
exchange that grouped correctly but
+/// sorted incorrectly is not a failure mode the exchange can produce - losing 
the sort loses the grouping too, which
+/// the reappearance check already catches.
+///
+/// ## Guardrails throw, they never truncate
+///
+/// [MatchLimits#MAX_ROWS_IN_MATCH] bounds the rows buffered for a partition 
and
+/// [MatchLimits#MAX_STEPS_PER_MATCH_ATTEMPT] bounds the backtracking of one 
match attempt. Both raise an error,
+/// because a truncated pattern result is a wrong result that nothing in the 
response would flag.
+///
+/// ## Not supported yet
+///
+/// `ALL ROWS PER MATCH` is rejected here as well as during planning: this 
operator emits exactly one row per
+/// match, so accepting it would silently return the wrong shape of result.
+public class MatchOperator extends MultiStageOperator {
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(MatchOperator.class);
+  private static final String EXPLAIN_NAME = "MATCH_RECOGNIZE";
+
+  private final MultiStageOperator _input;
+  private final DataSchema _resultSchema;
+  private final ColumnDataType[] _resultStoredTypes;
+  private final int[] _partitionKeys;
+  private final List<PatternSymbol> _patternSymbols;
+  private final MatchExpression[] _measures;
+  private final PartitionMatcher _matcher;
+  private final MatchNode.AfterMatchSkipMode _skipMode;
+  private final int _skipToSymbolOrdinal;
+  private final int _maxRowsInMatch;
+  private final StatMap<StatKey> _statMap = new StatMap<>(StatKey.class);
+
+  private final Set<Key> _closedPartitionKeys = new HashSet<>();

Review Comment:
   `_closedPartitionKeys` grows unbounded in the number of *distinct 
partitions* for the lifetime of the operator. `MatchLimits` caps 
rows-per-partition (`maxRowsInMatch`) and steps-per-attempt, but nothing bounds 
the partition count.
   
   A `PARTITION BY <high-cardinality col>` that lands many millions of tiny 
(even single-row) partitions on one worker retains one `Key` per closed 
partition, which can OOM even though every individual partition is well within 
`maxRowsInMatch`. Bounded-by-cardinality rather than a classic leak, since it's 
needed for the reappearance-detection guard — but worth a comment noting the 
growth, or a cap.
   
   (Not a correctness bug; the guard itself is a sound backstop for the 
exchange-clustering assumption.)



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

To unsubscribe, e-mail: [email protected]

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


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

Reply via email to