You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@flink.apache.org by "bvarghese1 (via GitHub)" <gi...@apache.org> on 2024/12/06 01:16:46 UTC
[PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
bvarghese1 opened a new pull request, #25753:
URL: https://github.com/apache/flink/pull/25753
<!--
*Thank you very much for contributing to Apache Flink - we are happy that you want to help us improve Flink. To help the community review your contribution in the best possible way, please go through the checklist below, which will get the contribution into a shape in which it can be best reviewed.*
*Please understand that we do not do this to make contributions to Flink a hassle. In order to uphold a high standard of quality for code contributions, while at the same time managing a large number of contributions, we need contributors to prepare the contributions well, and give reviewers enough contextual information for the review. Please also understand that contributions that do not follow this guide will take longer to review and thus typically be picked up with lower priority by the community.*
## Contribution Checklist
- Make sure that the pull request corresponds to a [JIRA issue](https://issues.apache.org/jira/projects/FLINK/issues). Exceptions are made for typos in JavaDoc or documentation files, which need no JIRA issue.
- Name the pull request in the form "[FLINK-XXXX] [component] Title of the pull request", where *FLINK-XXXX* should be replaced by the actual issue number. Skip *component* if you are unsure about which is the best component.
Typo fixes that have no associated JIRA issue should be named following this pattern: `[hotfix] [docs] Fix typo in event time introduction` or `[hotfix] [javadocs] Expand JavaDoc for PuncuatedWatermarkGenerator`.
- Fill out the template below to describe the changes contributed by the pull request. That will give reviewers the context they need to do the review.
- Make sure that the change passes the automated tests, i.e., `mvn clean verify` passes. You can set up Azure Pipelines CI to do that following [this guide](https://cwiki.apache.org/confluence/display/FLINK/Azure+Pipelines#AzurePipelines-Tutorial:SettingupAzurePipelinesforaforkoftheFlinkrepository).
- Each pull request should address only one issue, not mix up code from multiple issues.
- Each commit in the pull request has a meaningful commit message (including the JIRA id)
- Once all items of the checklist are addressed, remove the above text and this checklist, leaving only the filled out template below.
**(The sections below can be removed for hotfixes of typos)**
-->
## What is the purpose of the change
- Non-time attribute over aggregates can support retract mode
- Time attribute over aggregates support only append mode
- An example would be:
- `
INSERT INTO sink_t SELECT key, val, ts, SUM(val) OVER (PARTITION BY key ORDER BY <NON-TIME-ATTRIBUTE>
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS sum_val FROM source_t
`
## Brief change log
- The entire input state has to be maintained to retract old records correctly
- The state is maintained as ValueState<List<RowData>> per key
- Requires documentation (WIP)
## Verifying this change
Please make sure both new and modified tests in this PR follow [the conventions for tests defined in our code quality guide](https://flink.apache.org/how-to-contribute/code-style-and-quality-common/#7-testing).
*(Please pick either of the following options)*
- Added restore tests
## Does this pull request potentially affect one of the following parts:
- Dependencies (does it add or upgrade a dependency): (no)
- The public API, i.e., is any changed class annotated with `@Public(Evolving)`: (no)
- The serializers: (no)
- The runtime per-record code paths (performance sensitive): (yes)
- Anything that affects deployment or recovery: JobManager (and its components), Checkpointing, Kubernetes/Yarn, ZooKeeper: (no)
- The S3 file system connector: (no)
## Documentation
- Does this pull request introduce a new feature? (yes)
- If yes, how is the feature documented? (not documented)
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928128199
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ i++;
+ }
+ i = 0;
+ } else {
+ prevAcc = accMapState.get(ids.get(ids.size() - 2));
+ }
+
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ if (ids.get(i) == insertedId) {
+ sendUpdate(curValue, RowKind.INSERT, out);
+ } else {
+ sendUpdateBefore(curValue, prevAcc, out);
+ sendUpdateAfter(curValue, out);
+ if (i != 0 && insertedId == -1) {
+ iterator.next();
+ }
+ }
+ i++;
+ }
+ }
+
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ // Update single row
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ } else if (equaliser.equals(curValue, input)) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ removeRowFromSameSortKeys(sortedKeyList, curKey, iterator, ids, out);
+ } else {
+ // Retract single record
+ RowData curAcc = accMapState.get(curKey.getLong(keyIdx));
+ sendUpdateBefore(input, curAcc, out);
+ iterator.remove();
+ removeFromState(sortedKeyList, ids, curKey.getLong(keyIdx), -1, curKey.getLong(1));
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ } else {
+ currFunction.setAccumulators(currFunction.createAccumulators());
+ }
+ }
+ isRetracted = true;
+ }
+ }
+ }
+
+ private void removeRowFromSameSortKeys(List<RowData> sortedKeyList, RowData curKey, ListIterator<RowData> iterator, List<Long> ids, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ int removeIndex = -1;
+ while (i < ids.size()) {
+ if (ids.get(i) == curKey.getLong(keyIdx)) {
+ removeIndex = i;
+ break;
+ }
+ i++;
+ }
+
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.nextIndex() - 1 - i - 1);
+ RowData lastAcc = accMapState.get(ids.get(ids.size() - 1));
+ i = 0;
+ while (i < ids.size()) {
+ if (ids.get(i) == curKey.getLong(keyIdx)) {
+ // skip accumulating
+ } else {
+ RowData value = valueMapState.get(ids.get(i));
+ currFunction.accumulate(value);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ }
+ i++;
+ }
+
+ i = 0;
+ while (i < ids.size()) {
Review Comment:
New implementation uses for-loops.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928127022
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ i++;
+ }
+ i = 0;
+ } else {
+ prevAcc = accMapState.get(ids.get(ids.size() - 2));
+ }
+
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ if (ids.get(i) == insertedId) {
+ sendUpdate(curValue, RowKind.INSERT, out);
+ } else {
+ sendUpdateBefore(curValue, prevAcc, out);
+ sendUpdateAfter(curValue, out);
+ if (i != 0 && insertedId == -1) {
+ iterator.next();
+ }
+ }
+ i++;
+ }
+ }
+
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
Review Comment:
Yes, the new implementation does that.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928135183
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
Review Comment:
Fixed in the new implementation
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "fhueske (via GitHub)" <gi...@apache.org>.
fhueske commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1944924663
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
Review Comment:
nit: not just network bandwidth but also all downstream processing incl. writing the result to the sink system
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
Review Comment:
nit: not just network bandwidth but also all downstream processing incl. writing the result to the sink system
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
Review Comment:
nit: not just network bandwidth but also all downstream processing incl. writing the result to the sink system
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
Review Comment:
It might make sense to buffer the last fetched row.
That would avoid to read the row again from `valueMapState` in the common case, when there's just one row for the sort key.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
Review Comment:
this method is just used once. It could also be inlined in `insertIntoSortedList()`
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+
+ // update accumulated state for sortKey
+ accMapState.put(curSortKey, currAcc);
+ }
+ }
+
+ private void sendUpdateForChangedRow(
+ Collector<RowData> out,
+ RowKind rowKind,
+ RowData changedRow,
+ RowData prevAggValue,
+ RowData currAggValue) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, changedRow, prevAggValue);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, changedRow, rowKind, currAggValue);
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(
+ Collector<RowData> out, RowData value, RowKind rowKind, RowData currAggValue) {
+ output.setRowKind(rowKind);
+ output.replace(value, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAggValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching delRow from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param delRow
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
+ int i = 0;
+ boolean sortKeyFound = false;
+ delRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(delRow);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (; i < sortedList.size(); i++) {
Review Comment:
reuse `findIndexForInsertOrUpdate()` here (and give it a more generic name?)
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+
+ // update accumulated state for sortKey
+ accMapState.put(curSortKey, currAcc);
+ }
+ }
+
+ private void sendUpdateForChangedRow(
+ Collector<RowData> out,
+ RowKind rowKind,
+ RowData changedRow,
+ RowData prevAggValue,
+ RowData currAggValue) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, changedRow, prevAggValue);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, changedRow, rowKind, currAggValue);
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(
+ Collector<RowData> out, RowData value, RowKind rowKind, RowData currAggValue) {
+ output.setRowKind(rowKind);
+ output.replace(value, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAggValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching delRow from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param delRow
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
+ int i = 0;
+ boolean sortKeyFound = false;
+ delRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(delRow);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ sortKeyFound = true;
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, delRow);
+ if (removeIndex == -1) {
+ LOG.info(
+ "Could not find matching row to remove. Missing id from sortKey ids list.");
+ numOfIdsNotFound.inc();
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ RowKind.DELETE,
+ delRow,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(
+ sortedList, ids.size(), deletedId, curSortKey, aggFuncs.getAccumulators());
+ break;
+ }
+ }
+
+ if (!sortKeyFound) {
+ LOG.debug("Could not find matching sort key. Skipping delete.");
+ numOfSortKeysNotFound.inc();
+ }
+
+ processRemainingElements(sortedList, i, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids without the id that will be
+ * removed.
+ *
+ * @param ids
+ * @param delRow
+ * @return the index position of the id that should be removed
+ * @throws Exception
+ */
+ private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData delRow) throws Exception {
+ int removeIndex = -1;
+ for (int j = 0; j < ids.size(); j++) {
+ RowData curValue = valueMapState.get(ids.get(j));
+ if (valueEqualiser.equals(curValue, delRow)) {
+ removeIndex = j;
+ } else {
+ aggFuncs.accumulate(curValue);
+ }
+ }
+ return removeIndex;
+ }
+
+ /**
+ * Remove the ith element from the sortedList if the list of ids is empty and return the ith
+ * location OR update the sorted list with the list of ids and return the next location of the
+ * sortedList.
+ *
+ * @param sortedList
+ * @param idx
+ * @param ids
+ * @param curSortKey
+ * @return
+ */
+ private int removeIdFromSortedList(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int idx,
+ List<Long> ids,
+ RowData curSortKey) {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ // Update sortedList without the removed id
+ sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
+ idx++;
+ }
Review Comment:
```suggestion
// Update sortedList without the removed id
sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
idx++;
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
Review Comment:
great description!
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
Review Comment:
this method is just used once. It could also be inlined in `insertIntoSortedList()`
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
Review Comment:
```suggestion
int compResult = sortKeyComparator.compare(curSortKey, inputSortKey);
if (compResult == 0) {
```
Calling the comparator just once instead of twice for each entry in the list.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
Review Comment:
nit: could be moved after the accumulator equality check
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
Review Comment:
```suggestion
int compResult = sortKeyComparator.compare(curSortKey, inputSortKey);
if (compResult == 0) {
```
Calling the comparator just once instead of twice for each entry in the list.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
Review Comment:
```suggestion
int compResult = sortKeyComparator.compare(curSortKey, inputSortKey);
if (compResult == 0) {
```
Calling the comparator just once instead of twice for each entry in the list.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
Review Comment:
nit: could be moved after the accumulator equality check
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
Review Comment:
```suggestion
int compResult = sortKeyComparator.compare(curSortKey, inputSortKey);
if (compResult == 0) {
```
Calling the comparator just once instead of twice for each entry in the list.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
Review Comment:
```suggestion
// Update acc value for all ids except the last one (which is the newly inserted rows)
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
Review Comment:
nit: could be moved after the accumulator equality check
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
Review Comment:
nit: could be moved after the accumulator equality check
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
Review Comment:
```suggestion
int compResult = sortKeyComparator.compare(curSortKey, inputSortKey);
if (compResult == 0) {
```
Calling the comparator just once instead of twice for each entry in the list.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
Review Comment:
this method is just used once. It could also be inlined in `insertIntoSortedList()`
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
Review Comment:
```suggestion
} else if (compResult > 0) {
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
Review Comment:
```suggestion
return;
```
a bit more explicit that we leave the method in this case
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
Review Comment:
It might make sense to buffer the last fetched row.
That would avoid to read the row again from `valueMapState` in the common case, when there's just one row for the sort key.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
Review Comment:
It might make sense to buffer the last fetched row.
That would avoid to read the row again from `valueMapState` in the common case, when there's just one row for the sort key.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+
+ // update accumulated state for sortKey
+ accMapState.put(curSortKey, currAcc);
+ }
+ }
+
+ private void sendUpdateForChangedRow(
+ Collector<RowData> out,
+ RowKind rowKind,
+ RowData changedRow,
+ RowData prevAggValue,
+ RowData currAggValue) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, changedRow, prevAggValue);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, changedRow, rowKind, currAggValue);
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(
+ Collector<RowData> out, RowData value, RowKind rowKind, RowData currAggValue) {
+ output.setRowKind(rowKind);
+ output.replace(value, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAggValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching delRow from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param delRow
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
+ int i = 0;
+ boolean sortKeyFound = false;
+ delRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(delRow);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (; i < sortedList.size(); i++) {
Review Comment:
reuse `findIndexForInsertOrUpdate()` here (and give it a more generic name?)
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+
+ // update accumulated state for sortKey
+ accMapState.put(curSortKey, currAcc);
+ }
+ }
+
+ private void sendUpdateForChangedRow(
+ Collector<RowData> out,
+ RowKind rowKind,
+ RowData changedRow,
+ RowData prevAggValue,
+ RowData currAggValue) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, changedRow, prevAggValue);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, changedRow, rowKind, currAggValue);
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(
+ Collector<RowData> out, RowData value, RowKind rowKind, RowData currAggValue) {
+ output.setRowKind(rowKind);
+ output.replace(value, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAggValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching delRow from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param delRow
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
+ int i = 0;
+ boolean sortKeyFound = false;
+ delRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(delRow);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ sortKeyFound = true;
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, delRow);
+ if (removeIndex == -1) {
+ LOG.info(
+ "Could not find matching row to remove. Missing id from sortKey ids list.");
+ numOfIdsNotFound.inc();
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ RowKind.DELETE,
+ delRow,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(
+ sortedList, ids.size(), deletedId, curSortKey, aggFuncs.getAccumulators());
+ break;
+ }
+ }
+
+ if (!sortKeyFound) {
+ LOG.debug("Could not find matching sort key. Skipping delete.");
+ numOfSortKeysNotFound.inc();
+ }
+
+ processRemainingElements(sortedList, i, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids without the id that will be
+ * removed.
+ *
+ * @param ids
+ * @param delRow
+ * @return the index position of the id that should be removed
+ * @throws Exception
+ */
+ private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData delRow) throws Exception {
+ int removeIndex = -1;
+ for (int j = 0; j < ids.size(); j++) {
+ RowData curValue = valueMapState.get(ids.get(j));
+ if (valueEqualiser.equals(curValue, delRow)) {
+ removeIndex = j;
+ } else {
+ aggFuncs.accumulate(curValue);
+ }
+ }
+ return removeIndex;
+ }
+
+ /**
+ * Remove the ith element from the sortedList if the list of ids is empty and return the ith
+ * location OR update the sorted list with the list of ids and return the next location of the
+ * sortedList.
+ *
+ * @param sortedList
+ * @param idx
+ * @param ids
+ * @param curSortKey
+ * @return
+ */
+ private int removeIdFromSortedList(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int idx,
+ List<Long> ids,
+ RowData curSortKey) {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ // Update sortedList without the removed id
+ sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
+ idx++;
+ }
+ }
+ return idx;
+ }
+
+ private void removeFromState(
Review Comment:
this method is just used once and not very complex.
It could be inlined with `removeFromSortedList()` to have the state update logic a more "visible".
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+
+ // update accumulated state for sortKey
+ accMapState.put(curSortKey, currAcc);
+ }
+ }
+
+ private void sendUpdateForChangedRow(
+ Collector<RowData> out,
+ RowKind rowKind,
+ RowData changedRow,
+ RowData prevAggValue,
+ RowData currAggValue) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, changedRow, prevAggValue);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, changedRow, rowKind, currAggValue);
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(
+ Collector<RowData> out, RowData value, RowKind rowKind, RowData currAggValue) {
+ output.setRowKind(rowKind);
+ output.replace(value, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAggValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching delRow from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param delRow
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
+ int i = 0;
+ boolean sortKeyFound = false;
+ delRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(delRow);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ sortKeyFound = true;
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, delRow);
+ if (removeIndex == -1) {
+ LOG.info(
+ "Could not find matching row to remove. Missing id from sortKey ids list.");
+ numOfIdsNotFound.inc();
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ RowKind.DELETE,
+ delRow,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(
+ sortedList, ids.size(), deletedId, curSortKey, aggFuncs.getAccumulators());
+ break;
+ }
+ }
+
+ if (!sortKeyFound) {
+ LOG.debug("Could not find matching sort key. Skipping delete.");
+ numOfSortKeysNotFound.inc();
+ }
+
+ processRemainingElements(sortedList, i, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids without the id that will be
+ * removed.
+ *
+ * @param ids
+ * @param delRow
+ * @return the index position of the id that should be removed
+ * @throws Exception
+ */
+ private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData delRow) throws Exception {
+ int removeIndex = -1;
+ for (int j = 0; j < ids.size(); j++) {
+ RowData curValue = valueMapState.get(ids.get(j));
+ if (valueEqualiser.equals(curValue, delRow)) {
+ removeIndex = j;
+ } else {
+ aggFuncs.accumulate(curValue);
+ }
+ }
+ return removeIndex;
+ }
+
+ /**
+ * Remove the ith element from the sortedList if the list of ids is empty and return the ith
+ * location OR update the sorted list with the list of ids and return the next location of the
+ * sortedList.
+ *
+ * @param sortedList
+ * @param idx
+ * @param ids
+ * @param curSortKey
+ * @return
+ */
+ private int removeIdFromSortedList(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int idx,
+ List<Long> ids,
+ RowData curSortKey) {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ // Update sortedList without the removed id
+ sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
+ idx++;
+ }
Review Comment:
```suggestion
// Update sortedList without the removed id
sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
idx++;
```
`ids.isEmpty()` was already checked before.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+
+ // update accumulated state for sortKey
+ accMapState.put(curSortKey, currAcc);
+ }
+ }
+
+ private void sendUpdateForChangedRow(
+ Collector<RowData> out,
+ RowKind rowKind,
+ RowData changedRow,
+ RowData prevAggValue,
+ RowData currAggValue) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, changedRow, prevAggValue);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, changedRow, rowKind, currAggValue);
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(
+ Collector<RowData> out, RowData value, RowKind rowKind, RowData currAggValue) {
+ output.setRowKind(rowKind);
+ output.replace(value, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAggValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching delRow from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param delRow
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
+ int i = 0;
+ boolean sortKeyFound = false;
+ delRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(delRow);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ sortKeyFound = true;
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, delRow);
+ if (removeIndex == -1) {
+ LOG.info(
+ "Could not find matching row to remove. Missing id from sortKey ids list.");
+ numOfIdsNotFound.inc();
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ RowKind.DELETE,
+ delRow,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(
+ sortedList, ids.size(), deletedId, curSortKey, aggFuncs.getAccumulators());
+ break;
+ }
+ }
+
+ if (!sortKeyFound) {
+ LOG.debug("Could not find matching sort key. Skipping delete.");
+ numOfSortKeysNotFound.inc();
Review Comment:
```suggestion
numOfSortKeysNotFound.inc();
return;
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+
+ // update accumulated state for sortKey
+ accMapState.put(curSortKey, currAcc);
+ }
+ }
+
+ private void sendUpdateForChangedRow(
+ Collector<RowData> out,
+ RowKind rowKind,
+ RowData changedRow,
+ RowData prevAggValue,
+ RowData currAggValue) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, changedRow, prevAggValue);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, changedRow, rowKind, currAggValue);
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(
+ Collector<RowData> out, RowData value, RowKind rowKind, RowData currAggValue) {
+ output.setRowKind(rowKind);
+ output.replace(value, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAggValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching delRow from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param delRow
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
+ int i = 0;
+ boolean sortKeyFound = false;
+ delRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(delRow);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ sortKeyFound = true;
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, delRow);
+ if (removeIndex == -1) {
+ LOG.info(
+ "Could not find matching row to remove. Missing id from sortKey ids list.");
+ numOfIdsNotFound.inc();
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ RowKind.DELETE,
+ delRow,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(
+ sortedList, ids.size(), deletedId, curSortKey, aggFuncs.getAccumulators());
+ break;
+ }
+ }
+
+ if (!sortKeyFound) {
+ LOG.debug("Could not find matching sort key. Skipping delete.");
+ numOfSortKeysNotFound.inc();
+ }
+
+ processRemainingElements(sortedList, i, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids without the id that will be
+ * removed.
+ *
+ * @param ids
+ * @param delRow
+ * @return the index position of the id that should be removed
+ * @throws Exception
+ */
+ private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData delRow) throws Exception {
+ int removeIndex = -1;
+ for (int j = 0; j < ids.size(); j++) {
+ RowData curValue = valueMapState.get(ids.get(j));
+ if (valueEqualiser.equals(curValue, delRow)) {
+ removeIndex = j;
+ } else {
+ aggFuncs.accumulate(curValue);
+ }
+ }
+ return removeIndex;
+ }
+
+ /**
+ * Remove the ith element from the sortedList if the list of ids is empty and return the ith
+ * location OR update the sorted list with the list of ids and return the next location of the
+ * sortedList.
+ *
+ * @param sortedList
+ * @param idx
+ * @param ids
+ * @param curSortKey
+ * @return
+ */
+ private int removeIdFromSortedList(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int idx,
+ List<Long> ids,
+ RowData curSortKey) {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ // Update sortedList without the removed id
+ sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
+ idx++;
+ }
+ }
+ return idx;
+ }
+
+ private void removeFromState(
Review Comment:
this method is just used once and not very complex.
It could be inlined with `removeFromSortedList()` to have the state update logic a more "visible".
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+
+ // update accumulated state for sortKey
+ accMapState.put(curSortKey, currAcc);
+ }
+ }
+
+ private void sendUpdateForChangedRow(
+ Collector<RowData> out,
+ RowKind rowKind,
+ RowData changedRow,
+ RowData prevAggValue,
+ RowData currAggValue) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, changedRow, prevAggValue);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, changedRow, rowKind, currAggValue);
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(
+ Collector<RowData> out, RowData value, RowKind rowKind, RowData currAggValue) {
+ output.setRowKind(rowKind);
+ output.replace(value, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAggValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching delRow from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param delRow
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
+ int i = 0;
+ boolean sortKeyFound = false;
+ delRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(delRow);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ sortKeyFound = true;
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, delRow);
+ if (removeIndex == -1) {
+ LOG.info(
+ "Could not find matching row to remove. Missing id from sortKey ids list.");
+ numOfIdsNotFound.inc();
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ RowKind.DELETE,
+ delRow,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(
+ sortedList, ids.size(), deletedId, curSortKey, aggFuncs.getAccumulators());
+ break;
+ }
+ }
+
+ if (!sortKeyFound) {
+ LOG.debug("Could not find matching sort key. Skipping delete.");
+ numOfSortKeysNotFound.inc();
+ }
+
+ processRemainingElements(sortedList, i, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids without the id that will be
+ * removed.
+ *
+ * @param ids
+ * @param delRow
+ * @return the index position of the id that should be removed
+ * @throws Exception
+ */
+ private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData delRow) throws Exception {
+ int removeIndex = -1;
+ for (int j = 0; j < ids.size(); j++) {
+ RowData curValue = valueMapState.get(ids.get(j));
+ if (valueEqualiser.equals(curValue, delRow)) {
+ removeIndex = j;
+ } else {
+ aggFuncs.accumulate(curValue);
+ }
+ }
+ return removeIndex;
+ }
+
+ /**
+ * Remove the ith element from the sortedList if the list of ids is empty and return the ith
+ * location OR update the sorted list with the list of ids and return the next location of the
+ * sortedList.
+ *
+ * @param sortedList
+ * @param idx
+ * @param ids
+ * @param curSortKey
+ * @return
+ */
+ private int removeIdFromSortedList(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int idx,
+ List<Long> ids,
+ RowData curSortKey) {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ // Update sortedList without the removed id
+ sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
+ idx++;
+ }
Review Comment:
```suggestion
// Update sortedList without the removed id
sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
idx++;
```
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1922728048
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
Review Comment:
Renamed to ids
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1908082828
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
Review Comment:
Moved to `insertRow` helper function
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928127870
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ i++;
+ }
+ i = 0;
+ } else {
+ prevAcc = accMapState.get(ids.get(ids.size() - 2));
+ }
+
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ if (ids.get(i) == insertedId) {
+ sendUpdate(curValue, RowKind.INSERT, out);
+ } else {
+ sendUpdateBefore(curValue, prevAcc, out);
+ sendUpdateAfter(curValue, out);
+ if (i != 0 && insertedId == -1) {
+ iterator.next();
+ }
+ }
+ i++;
+ }
+ }
+
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ // Update single row
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ } else if (equaliser.equals(curValue, input)) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ removeRowFromSameSortKeys(sortedKeyList, curKey, iterator, ids, out);
+ } else {
+ // Retract single record
+ RowData curAcc = accMapState.get(curKey.getLong(keyIdx));
+ sendUpdateBefore(input, curAcc, out);
+ iterator.remove();
+ removeFromState(sortedKeyList, ids, curKey.getLong(keyIdx), -1, curKey.getLong(1));
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ } else {
+ currFunction.setAccumulators(currFunction.createAccumulators());
+ }
+ }
+ isRetracted = true;
+ }
+ }
+ }
+
+ private void removeRowFromSameSortKeys(List<RowData> sortedKeyList, RowData curKey, ListIterator<RowData> iterator, List<Long> ids, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ int removeIndex = -1;
+ while (i < ids.size()) {
+ if (ids.get(i) == curKey.getLong(keyIdx)) {
+ removeIndex = i;
+ break;
+ }
+ i++;
+ }
+
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.nextIndex() - 1 - i - 1);
Review Comment:
Removed this and no longer exists in the new implementation.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928131044
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ i++;
+ }
+ i = 0;
+ } else {
+ prevAcc = accMapState.get(ids.get(ids.size() - 2));
+ }
+
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ if (ids.get(i) == insertedId) {
+ sendUpdate(curValue, RowKind.INSERT, out);
+ } else {
+ sendUpdateBefore(curValue, prevAcc, out);
+ sendUpdateAfter(curValue, out);
+ if (i != 0 && insertedId == -1) {
+ iterator.next();
+ }
Review Comment:
This does not happen in the new implementation
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928138032
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
Review Comment:
Added a test using AVG function
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1907358904
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ insertIntoSortedListOptimized(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ removeFromSortedListOptimized(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
Review Comment:
Removed retractable accumulators since the new implementation also stores the accumulators
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1907587113
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ insertIntoSortedListOptimized(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ removeFromSortedListOptimized(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
Review Comment:
This is being done as part of the new implementation.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Add NonTimeRangeUnboundedPrecedingFunction for OverAggregate [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r2012510871
##########
flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/OverWindowRestoreTest.java:
##########
@@ -36,7 +36,7 @@ public OverWindowRestoreTest() {
@Override
protected Stream<String> getSavepointPaths(
TableTestProgram program, ExecNodeMetadata metadata) {
- if (metadata.version() == 1) {
+ if (metadata.version() == 1 && program.equals(OverWindowTestPrograms.LAG_OVER_FUNCTION)) {
Review Comment:
Added comment to clarify this.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1907587955
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ insertIntoSortedListOptimized(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ removeFromSortedListOptimized(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
Review Comment:
Persisting accumulators for each row
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1908078349
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
Review Comment:
Tested the getValue() and getAccumulators() for AVG. However, both return a tuple for AVG
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1908082185
##########
flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunctionTest.java:
##########
@@ -0,0 +1,467 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.streaming.api.operators.KeyedProcessOperator;
+import org.apache.flink.streaming.api.watermark.Watermark;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordComparator;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.types.RowKind;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.insertRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateAfterRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateBeforeRecord;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for {@link NonTimeUnboundedPrecedingFunction}. */
+public class NonTimeUnboundedPrecedingFunctionTest extends RowTimeOverWindowTestBase {
+
+ private static final int SORT_KEY_IDX = 1;
+
+ private static final FieldGetter SORT_KEY_FIELD_GETTER =
+ RowData.createFieldGetter(DataTypes.BIGINT().getLogicalType(), SORT_KEY_IDX);
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(SORT_KEY_IDX);
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_RECORD_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ // return new RowDataRecordEqualiser();
+ return new TestRecordEqualiser();
+ }
+ };
+
+ /** Custom test comparator for comparing numbers. */
+ public static class LongRecordComparator implements RecordComparator {
+
+ private int pos;
+
+ public LongRecordComparator(int pos) {
+ this.pos = pos;
+ }
+
+ @Override
+ public int compare(RowData o1, RowData o2) {
+ boolean null0At1 = o1.isNullAt(pos);
+ boolean null0At2 = o2.isNullAt(pos);
+ int cmp0 =
+ null0At1 && null0At2
+ ? 0
+ : (null0At1
+ ? -1
+ : (null0At2
+ ? 1
+ : Long.compare(o1.getLong(pos), o2.getLong(pos))));
+ if (cmp0 != 0) {
+ return cmp0;
+ }
+ return 0;
+ }
+ }
+
+ /** Custom test row equaliser for comparing rows. */
+ public static class TestRecordEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getString(0).equals(row2.getString(0))
+ && row1.getLong(1) == row2.getLong(1)
+ && row1.getLong(2) == row2.getLong(2);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ @Test
+ public void testInsertOnlyRecordsWithCustomSortKey() throws Exception {
+ NonTimeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeUnboundedPrecedingFunction<RowData>(
+ 0,
+ 2000,
+ aggsHandleFunction,
Review Comment:
Added a test for AVG. However, using accumulators and values interchangeably worked. For consistency, I have made the change to use accumulators while comparing and storing.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1953814652
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+
+ // update accumulated state for sortKey
+ accMapState.put(curSortKey, currAcc);
+ }
+ }
+
+ private void sendUpdateForChangedRow(
+ Collector<RowData> out,
+ RowKind rowKind,
+ RowData changedRow,
+ RowData prevAggValue,
+ RowData currAggValue) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, changedRow, prevAggValue);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, changedRow, rowKind, currAggValue);
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(
+ Collector<RowData> out, RowData value, RowKind rowKind, RowData currAggValue) {
+ output.setRowKind(rowKind);
+ output.replace(value, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAggValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching delRow from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param delRow
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
+ int i = 0;
+ boolean sortKeyFound = false;
+ delRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(delRow);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ sortKeyFound = true;
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, delRow);
+ if (removeIndex == -1) {
+ LOG.info(
+ "Could not find matching row to remove. Missing id from sortKey ids list.");
+ numOfIdsNotFound.inc();
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ RowKind.DELETE,
+ delRow,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(
+ sortedList, ids.size(), deletedId, curSortKey, aggFuncs.getAccumulators());
+ break;
+ }
+ }
+
+ if (!sortKeyFound) {
+ LOG.debug("Could not find matching sort key. Skipping delete.");
+ numOfSortKeysNotFound.inc();
+ }
+
+ processRemainingElements(sortedList, i, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids without the id that will be
+ * removed.
+ *
+ * @param ids
+ * @param delRow
+ * @return the index position of the id that should be removed
+ * @throws Exception
+ */
+ private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData delRow) throws Exception {
+ int removeIndex = -1;
+ for (int j = 0; j < ids.size(); j++) {
+ RowData curValue = valueMapState.get(ids.get(j));
+ if (valueEqualiser.equals(curValue, delRow)) {
+ removeIndex = j;
+ } else {
+ aggFuncs.accumulate(curValue);
+ }
+ }
+ return removeIndex;
+ }
+
+ /**
+ * Remove the ith element from the sortedList if the list of ids is empty and return the ith
+ * location OR update the sorted list with the list of ids and return the next location of the
+ * sortedList.
+ *
+ * @param sortedList
+ * @param idx
+ * @param ids
+ * @param curSortKey
+ * @return
+ */
+ private int removeIdFromSortedList(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int idx,
+ List<Long> ids,
+ RowData curSortKey) {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ // Update sortedList without the removed id
+ sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
+ idx++;
+ }
Review Comment:
My bad. Fixed.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928126755
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ i++;
+ }
+ i = 0;
+ } else {
+ prevAcc = accMapState.get(ids.get(ids.size() - 2));
+ }
+
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ if (ids.get(i) == insertedId) {
+ sendUpdate(curValue, RowKind.INSERT, out);
+ } else {
+ sendUpdateBefore(curValue, prevAcc, out);
+ sendUpdateAfter(curValue, out);
+ if (i != 0 && insertedId == -1) {
+ iterator.next();
+ }
+ }
+ i++;
+ }
+ }
+
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ // Update single row
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ } else if (equaliser.equals(curValue, input)) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ removeRowFromSameSortKeys(sortedKeyList, curKey, iterator, ids, out);
+ } else {
+ // Retract single record
+ RowData curAcc = accMapState.get(curKey.getLong(keyIdx));
+ sendUpdateBefore(input, curAcc, out);
Review Comment:
Added a TODO for now.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928134831
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+
+ currFunction.accumulate(curValue);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
Review Comment:
Added this optimization in the new implemenation
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+
+ currFunction.accumulate(curValue);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
Review Comment:
Added this optimization in the new implementation
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Add NonTimeRangeUnboundedPrecedingFunction for OverAggregate [flink]
Posted by "twalthr (via GitHub)" <gi...@apache.org>.
twalthr merged PR #25753:
URL: https://github.com/apache/flink/pull/25753
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Add NonTimeRangeUnboundedPrecedingFunction for OverAggregate [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r2012514549
##########
docs/content/docs/dev/table/sql/queries/over-agg.md:
##########
@@ -70,11 +70,20 @@ There are two options to define the range, `ROWS` intervals and `RANGE` interval
#### RANGE intervals
-A `RANGE` interval is defined on the values of the ORDER BY column, which is in case of Flink always a time attribute. The following RANGE interval defines that all rows with a time attribute of at most 30 minutes less than the current row are included in the aggregate.
+A `RANGE` interval is defined on the values of the ORDER BY column, which is in case of Flink is either a time or non-time attribute.
+
+The following RANGE interval defines that all rows with a time attribute of at most 30 minutes less than the current row are included in the aggregate.
```sql
RANGE BETWEEN INTERVAL '30' MINUTE PRECEDING AND CURRENT ROW
```
+The following RANGE interval defines that all rows with a non-time attribute of unbounded rows preceding the current row are included in the aggregate.
+```sql
+RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
+```
+
Review Comment:
Removed empty lines
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "fhueske (via GitHub)" <gi...@apache.org>.
fhueske commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1955836991
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,689 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.StateTtlConfig;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+import static org.apache.flink.table.runtime.util.StateConfigUtil.createTtlConfig;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunction<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final long stateRetentionTime;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
Review Comment:
```suggestion
private final GeneratedRecordComparator generatedSortKeyComparator;
```
##########
flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunctionTest.java:
##########
@@ -0,0 +1,2028 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.streaming.api.operators.KeyedProcessOperator;
+import org.apache.flink.streaming.api.watermark.Watermark;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordComparator;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.BigIntType;
+import org.apache.flink.table.types.logical.IntType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.VarCharType;
+import org.apache.flink.table.utils.HandwrittenSelectorUtil;
+import org.apache.flink.types.RowKind;
+
+import org.junit.Test;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.insertRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateAfterRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateBeforeRecord;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for {@link NonTimeRangeUnboundedPrecedingFunction}. */
+public class NonTimeRangeUnboundedPrecedingFunctionTest extends RowTimeOverWindowTestBase {
+
+ private static final int SORT_KEY_IDX = 1;
+
+ private static final LogicalType[] SORT_KEY_TYPES = new LogicalType[] {new BigIntType()};
+
+ private static final InternalTypeInfo<RowData> INPUT_ROW_TYPE =
+ InternalTypeInfo.ofFields(VarCharType.STRING_TYPE, new BigIntType(), new IntType());
Review Comment:
```suggestion
InternalTypeInfo.ofFields(VarCharType.STRING_TYPE, new BigIntType(), new BigIntType());
```
##########
flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunctionTest.java:
##########
@@ -0,0 +1,2028 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.streaming.api.operators.KeyedProcessOperator;
+import org.apache.flink.streaming.api.watermark.Watermark;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordComparator;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.BigIntType;
+import org.apache.flink.table.types.logical.IntType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.VarCharType;
+import org.apache.flink.table.utils.HandwrittenSelectorUtil;
+import org.apache.flink.types.RowKind;
+
+import org.junit.Test;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.insertRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateAfterRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateBeforeRecord;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for {@link NonTimeRangeUnboundedPrecedingFunction}. */
+public class NonTimeRangeUnboundedPrecedingFunctionTest extends RowTimeOverWindowTestBase {
+
+ private static final int SORT_KEY_IDX = 1;
+
+ private static final LogicalType[] SORT_KEY_TYPES = new LogicalType[] {new BigIntType()};
+
+ private static final InternalTypeInfo<RowData> INPUT_ROW_TYPE =
+ InternalTypeInfo.ofFields(VarCharType.STRING_TYPE, new BigIntType(), new IntType());
+
+ private static final RowDataKeySelector SORT_KEY_SELECTOR =
+ HandwrittenSelectorUtil.getRowDataSelector(
+ new int[] {SORT_KEY_IDX}, INPUT_ROW_TYPE.toRowFieldTypes());
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR_ASC =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(0, true);
+ }
+ };
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR_DESC =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(0, false);
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_ROW_VALUE_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ return new TestRowValueEqualiser();
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_SORT_KEY_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ return new TestSortKeyEqualiser();
+ }
+ };
+
+ /** Custom test comparator for comparing numbers. */
+ public static class LongRecordComparator implements RecordComparator {
+
+ private int pos;
+ private boolean isAscending;
+
+ public LongRecordComparator(int pos, boolean isAscending) {
+ this.pos = pos;
+ this.isAscending = isAscending;
+ }
+
+ @Override
+ public int compare(RowData o1, RowData o2) {
+ boolean null0At1 = o1.isNullAt(pos);
+ boolean null0At2 = o2.isNullAt(pos);
+ int cmp0 =
+ null0At1 && null0At2
+ ? 0
+ : (null0At1
+ ? -1
+ : (null0At2
+ ? 1
+ : Long.compare(o1.getLong(pos), o2.getLong(pos))));
+ if (cmp0 != 0) {
+ if (isAscending) {
+ return cmp0;
+ } else {
+ return -cmp0;
+ }
+ }
+ return 0;
+ }
+ }
+
+ /** Custom test row equaliser for comparing rows. */
+ public static class TestRowValueEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getString(0).equals(row2.getString(0))
+ && row1.getLong(1) == row2.getLong(1)
+ && row1.getLong(2) == row2.getLong(2);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ /** Custom test sortKey equaliser for comparing sort keys. */
+ public static class TestSortKeyEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getLong(0) == row2.getLong(0);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ @Test
+ public void testInsertOnlyRecordsWithCustomSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(insertRecord("key2", 1L, 100L));
+ testHarness.processElement(insertRecord("key2", 2L, 200L));
+
+ testHarness.processWatermark(new Watermark(500L));
+
+ // out of order record should trigger updates for all records after its inserted position
+ testHarness.processElement(insertRecord("key1", 4L, 400L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 14L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 7L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 12L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 14L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testInsertOnlyRecordsWithDuplicateSortKeys() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 5L, 502L));
+ testHarness.processElement(insertRecord("key1", 5L, 501L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(insertRecord("key2", 1L, 100L));
+ testHarness.processElement(insertRecord("key2", 2L, 200L));
+
+ testHarness.processWatermark(new Watermark(500L));
+
+ // out of order record should trigger updates for all records after its inserted position
+ testHarness.processElement(insertRecord("key1", 2L, 203L));
+ testHarness.processElement(insertRecord("key1", 2L, 201L));
+ testHarness.processElement(insertRecord("key1", 4L, 400L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 24L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 203L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 24L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 7L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 203L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 203L),
+ GenericRowData.ofKind(RowKind.INSERT, 7L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 7L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 28L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 11L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 28L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 32L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testRetractingRecordsWithCustomSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(updateBeforeRecord("key1", 2L, 200L));
+ testHarness.processElement(updateAfterRecord("key1", 3L, 200L));
+ testHarness.processElement(insertRecord("key2", 1L, 100L));
+ testHarness.processElement(insertRecord("key2", 2L, 200L));
+ testHarness.processElement(insertRecord("key3", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 4L, 400L));
+ testHarness.processElement(updateBeforeRecord("key1", 3L, 200L));
+ testHarness.processElement(updateAfterRecord("key1", 3L, 300L));
+
+ // Watermark has no impact and should be ignored
+ testHarness.processWatermark(new Watermark(500L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 14L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 6L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 14L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 12L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 3L,
+ 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 4L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 6L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 9L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 12L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key3"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 9L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 19L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 3L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 4L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 19L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 16L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 3L,
+ 300L),
+ GenericRowData.ofKind(RowKind.INSERT, 4L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 16L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 19L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testRetractWithFirstDuplicateSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 2L, 201L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 5L, 502L));
+ testHarness.processElement(insertRecord("key1", 5L, 501L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(updateBeforeRecord("key1", 5L, 500L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 21L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testRetractWithMiddleDuplicateSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 2L, 201L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 5L, 502L));
+ testHarness.processElement(insertRecord("key1", 5L, 501L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(updateBeforeRecord("key1", 5L, 502L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 21L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testRetractWithLastDuplicateSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 2L, 201L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 5L, 502L));
+ testHarness.processElement(insertRecord("key1", 5L, 501L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(updateBeforeRecord("key1", 5L, 501L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 21L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testRetractWithDescendingSort() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_DESC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 2L, 201L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(updateBeforeRecord("key1", 2L, 200L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 2L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 2L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 4L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 4L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 4L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 9L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 4L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 9L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 6L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 11L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 9L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 9L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 16L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 16L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 14L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testRetractWithEarlyOut() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 0L, 100L));
+ testHarness.processElement(insertRecord("key1", 0L, 101L));
+ testHarness.processElement(insertRecord("key1", 0L, 102L));
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 2L, 201L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 5L, 502L));
+ testHarness.processElement(insertRecord("key1", 5L, 501L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(updateBeforeRecord("key1", 0L, 100L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 0L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 0L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 0L, 101L),
+ GenericRowData.ofKind(RowKind.INSERT, 0L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 0L, 102L),
+ GenericRowData.ofKind(RowKind.INSERT, 0L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
Review Comment:
Do you also have tests with `RowKind.DELETE`?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,689 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.StateTtlConfig;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+import static org.apache.flink.table.runtime.util.StateConfigUtil.createTtlConfig;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunction<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final long stateRetentionTime;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+ @VisibleForTesting protected transient ValueStateDescriptor<Long> idStateDescriptor;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long stateRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
Review Comment:
```suggestion
GeneratedRecordComparator genSortKeyComparator,
```
##########
flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunctionTest.java:
##########
@@ -0,0 +1,2028 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.streaming.api.operators.KeyedProcessOperator;
+import org.apache.flink.streaming.api.watermark.Watermark;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordComparator;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.BigIntType;
+import org.apache.flink.table.types.logical.IntType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.VarCharType;
+import org.apache.flink.table.utils.HandwrittenSelectorUtil;
+import org.apache.flink.types.RowKind;
+
+import org.junit.Test;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.insertRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateAfterRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateBeforeRecord;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for {@link NonTimeRangeUnboundedPrecedingFunction}. */
+public class NonTimeRangeUnboundedPrecedingFunctionTest extends RowTimeOverWindowTestBase {
+
+ private static final int SORT_KEY_IDX = 1;
+
+ private static final LogicalType[] SORT_KEY_TYPES = new LogicalType[] {new BigIntType()};
+
+ private static final InternalTypeInfo<RowData> INPUT_ROW_TYPE =
+ InternalTypeInfo.ofFields(VarCharType.STRING_TYPE, new BigIntType(), new IntType());
+
+ private static final RowDataKeySelector SORT_KEY_SELECTOR =
+ HandwrittenSelectorUtil.getRowDataSelector(
+ new int[] {SORT_KEY_IDX}, INPUT_ROW_TYPE.toRowFieldTypes());
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR_ASC =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(0, true);
+ }
+ };
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR_DESC =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(0, false);
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_ROW_VALUE_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ return new TestRowValueEqualiser();
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_SORT_KEY_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ return new TestSortKeyEqualiser();
+ }
+ };
+
+ /** Custom test comparator for comparing numbers. */
+ public static class LongRecordComparator implements RecordComparator {
+
+ private int pos;
+ private boolean isAscending;
+
+ public LongRecordComparator(int pos, boolean isAscending) {
+ this.pos = pos;
+ this.isAscending = isAscending;
+ }
+
+ @Override
+ public int compare(RowData o1, RowData o2) {
+ boolean null0At1 = o1.isNullAt(pos);
+ boolean null0At2 = o2.isNullAt(pos);
+ int cmp0 =
+ null0At1 && null0At2
+ ? 0
+ : (null0At1
+ ? -1
+ : (null0At2
+ ? 1
+ : Long.compare(o1.getLong(pos), o2.getLong(pos))));
+ if (cmp0 != 0) {
+ if (isAscending) {
+ return cmp0;
+ } else {
+ return -cmp0;
+ }
+ }
+ return 0;
+ }
+ }
+
+ /** Custom test row equaliser for comparing rows. */
+ public static class TestRowValueEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getString(0).equals(row2.getString(0))
+ && row1.getLong(1) == row2.getLong(1)
+ && row1.getLong(2) == row2.getLong(2);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ /** Custom test sortKey equaliser for comparing sort keys. */
+ public static class TestSortKeyEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getLong(0) == row2.getLong(0);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ @Test
+ public void testInsertOnlyRecordsWithCustomSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(insertRecord("key2", 1L, 100L));
+ testHarness.processElement(insertRecord("key2", 2L, 200L));
+
+ testHarness.processWatermark(new Watermark(500L));
+
+ // out of order record should trigger updates for all records after its inserted position
+ testHarness.processElement(insertRecord("key1", 4L, 400L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
Review Comment:
A helper method that generates the `JoinedRowData` would reduce a lot of boilerplate code and make the tests more readable:
```
outputRecord(RowKind.INSERT, "key1", 1L, 100L, 1L),
outputRecord(RowKind.INSERT, "key1", 2L, 200L, 3L),
...
```
##########
flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunctionTest.java:
##########
@@ -0,0 +1,2028 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.streaming.api.operators.KeyedProcessOperator;
+import org.apache.flink.streaming.api.watermark.Watermark;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordComparator;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.BigIntType;
+import org.apache.flink.table.types.logical.IntType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.VarCharType;
+import org.apache.flink.table.utils.HandwrittenSelectorUtil;
+import org.apache.flink.types.RowKind;
+
+import org.junit.Test;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.insertRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateAfterRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateBeforeRecord;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for {@link NonTimeRangeUnboundedPrecedingFunction}. */
+public class NonTimeRangeUnboundedPrecedingFunctionTest extends RowTimeOverWindowTestBase {
+
+ private static final int SORT_KEY_IDX = 1;
+
+ private static final LogicalType[] SORT_KEY_TYPES = new LogicalType[] {new BigIntType()};
+
+ private static final InternalTypeInfo<RowData> INPUT_ROW_TYPE =
+ InternalTypeInfo.ofFields(VarCharType.STRING_TYPE, new BigIntType(), new IntType());
+
+ private static final RowDataKeySelector SORT_KEY_SELECTOR =
+ HandwrittenSelectorUtil.getRowDataSelector(
+ new int[] {SORT_KEY_IDX}, INPUT_ROW_TYPE.toRowFieldTypes());
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR_ASC =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(0, true);
+ }
+ };
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR_DESC =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(0, false);
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_ROW_VALUE_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ return new TestRowValueEqualiser();
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_SORT_KEY_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ return new TestSortKeyEqualiser();
+ }
+ };
+
+ /** Custom test comparator for comparing numbers. */
+ public static class LongRecordComparator implements RecordComparator {
+
+ private int pos;
+ private boolean isAscending;
+
+ public LongRecordComparator(int pos, boolean isAscending) {
+ this.pos = pos;
+ this.isAscending = isAscending;
+ }
+
+ @Override
+ public int compare(RowData o1, RowData o2) {
+ boolean null0At1 = o1.isNullAt(pos);
+ boolean null0At2 = o2.isNullAt(pos);
+ int cmp0 =
+ null0At1 && null0At2
+ ? 0
+ : (null0At1
+ ? -1
+ : (null0At2
+ ? 1
+ : Long.compare(o1.getLong(pos), o2.getLong(pos))));
+ if (cmp0 != 0) {
+ if (isAscending) {
+ return cmp0;
+ } else {
+ return -cmp0;
+ }
+ }
+ return 0;
+ }
+ }
+
+ /** Custom test row equaliser for comparing rows. */
+ public static class TestRowValueEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getString(0).equals(row2.getString(0))
+ && row1.getLong(1) == row2.getLong(1)
+ && row1.getLong(2) == row2.getLong(2);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ /** Custom test sortKey equaliser for comparing sort keys. */
+ public static class TestSortKeyEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getLong(0) == row2.getLong(0);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ @Test
+ public void testInsertOnlyRecordsWithCustomSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(insertRecord("key2", 1L, 100L));
+ testHarness.processElement(insertRecord("key2", 2L, 200L));
+
+ testHarness.processWatermark(new Watermark(500L));
Review Comment:
The operator is not acting on watermarks.
I think we can remove this.
##########
flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunctionTest.java:
##########
@@ -0,0 +1,2028 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.streaming.api.operators.KeyedProcessOperator;
+import org.apache.flink.streaming.api.watermark.Watermark;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordComparator;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.BigIntType;
+import org.apache.flink.table.types.logical.IntType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.VarCharType;
+import org.apache.flink.table.utils.HandwrittenSelectorUtil;
+import org.apache.flink.types.RowKind;
+
+import org.junit.Test;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.insertRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateAfterRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateBeforeRecord;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for {@link NonTimeRangeUnboundedPrecedingFunction}. */
+public class NonTimeRangeUnboundedPrecedingFunctionTest extends RowTimeOverWindowTestBase {
+
+ private static final int SORT_KEY_IDX = 1;
+
+ private static final LogicalType[] SORT_KEY_TYPES = new LogicalType[] {new BigIntType()};
+
+ private static final InternalTypeInfo<RowData> INPUT_ROW_TYPE =
+ InternalTypeInfo.ofFields(VarCharType.STRING_TYPE, new BigIntType(), new IntType());
+
+ private static final RowDataKeySelector SORT_KEY_SELECTOR =
+ HandwrittenSelectorUtil.getRowDataSelector(
+ new int[] {SORT_KEY_IDX}, INPUT_ROW_TYPE.toRowFieldTypes());
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR_ASC =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(0, true);
+ }
+ };
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR_DESC =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(0, false);
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_ROW_VALUE_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ return new TestRowValueEqualiser();
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_SORT_KEY_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ return new TestSortKeyEqualiser();
+ }
+ };
+
+ /** Custom test comparator for comparing numbers. */
+ public static class LongRecordComparator implements RecordComparator {
+
+ private int pos;
+ private boolean isAscending;
+
+ public LongRecordComparator(int pos, boolean isAscending) {
+ this.pos = pos;
+ this.isAscending = isAscending;
+ }
+
+ @Override
+ public int compare(RowData o1, RowData o2) {
+ boolean null0At1 = o1.isNullAt(pos);
+ boolean null0At2 = o2.isNullAt(pos);
+ int cmp0 =
+ null0At1 && null0At2
+ ? 0
+ : (null0At1
+ ? -1
+ : (null0At2
+ ? 1
+ : Long.compare(o1.getLong(pos), o2.getLong(pos))));
+ if (cmp0 != 0) {
+ if (isAscending) {
+ return cmp0;
+ } else {
+ return -cmp0;
+ }
+ }
+ return 0;
+ }
+ }
+
+ /** Custom test row equaliser for comparing rows. */
+ public static class TestRowValueEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getString(0).equals(row2.getString(0))
+ && row1.getLong(1) == row2.getLong(1)
+ && row1.getLong(2) == row2.getLong(2);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ /** Custom test sortKey equaliser for comparing sort keys. */
+ public static class TestSortKeyEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getLong(0) == row2.getLong(0);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ @Test
+ public void testInsertOnlyRecordsWithCustomSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(insertRecord("key2", 1L, 100L));
+ testHarness.processElement(insertRecord("key2", 2L, 200L));
+
+ testHarness.processWatermark(new Watermark(500L));
+
+ // out of order record should trigger updates for all records after its inserted position
+ testHarness.processElement(insertRecord("key1", 4L, 400L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 14L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 7L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 12L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 14L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testInsertOnlyRecordsWithDuplicateSortKeys() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 5L, 502L));
+ testHarness.processElement(insertRecord("key1", 5L, 501L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(insertRecord("key2", 1L, 100L));
+ testHarness.processElement(insertRecord("key2", 2L, 200L));
+
+ testHarness.processWatermark(new Watermark(500L));
+
+ // out of order record should trigger updates for all records after its inserted position
+ testHarness.processElement(insertRecord("key1", 2L, 203L));
+ testHarness.processElement(insertRecord("key1", 2L, 201L));
+ testHarness.processElement(insertRecord("key1", 4L, 400L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 24L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 203L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 24L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 7L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 203L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 203L),
+ GenericRowData.ofKind(RowKind.INSERT, 7L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 7L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 28L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 11L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 28L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 32L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testRetractingRecordsWithCustomSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(updateBeforeRecord("key1", 2L, 200L));
+ testHarness.processElement(updateAfterRecord("key1", 3L, 200L));
+ testHarness.processElement(insertRecord("key2", 1L, 100L));
+ testHarness.processElement(insertRecord("key2", 2L, 200L));
+ testHarness.processElement(insertRecord("key3", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 4L, 400L));
+ testHarness.processElement(updateBeforeRecord("key1", 3L, 200L));
+ testHarness.processElement(updateAfterRecord("key1", 3L, 300L));
+
+ // Watermark has no impact and should be ignored
+ testHarness.processWatermark(new Watermark(500L));
+
+ List<RowData> expectedRows =
Review Comment:
For such long sequences, it would be nice if we could evaluate the input in smaller chunks and compare the output in smaller batches. That would be easier to follow.
Not sure if the TestHarness is able to process and fetch results in multiple batches though...
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "fhueske (via GitHub)" <gi...@apache.org>.
fhueske commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1916711228
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
Review Comment:
the `sortKeyComparator` does not include the `id` in its comparison, right?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
Review Comment:
This is still hardcoded to `long` sortkeys, right?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
Review Comment:
wouldn't it be easier to store accumulators per sort key?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
Review Comment:
rename the method to make clear that it also emits rows
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
Review Comment:
use a `for (int id : ids)` loop which is much easier to read?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
Review Comment:
the names of the helper methods make it hard to distinguish the logic.
Some are called `send...`, other are called `update...` but still send updates.
more consistent naming and JavaDocs would help to understand the logic better
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
Review Comment:
We are emitting the accumulator here, but it should be the value of the aggregation function.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
Review Comment:
We don't need the full `curKey` in this method but just the row id (`curKey.getLong(keyIdx)`)
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
Review Comment:
I would not pass the accumulator but the aggregated value in.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
Review Comment:
I think it would be better to pass the aggregated value (`currFunction.getValue()`) into the method instead of accessing it from the `currFunction` member variable.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
Review Comment:
I'm not sure if mixing the two different usages of this method in the same code and distinguish it with `insertedId == -1`. IMO, it makes the code hard to follow.
Can we structure the code differently to make it easier to follow?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ i++;
+ }
+ i = 0;
+ } else {
+ prevAcc = accMapState.get(ids.get(ids.size() - 2));
+ }
+
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ if (ids.get(i) == insertedId) {
+ sendUpdate(curValue, RowKind.INSERT, out);
+ } else {
+ sendUpdateBefore(curValue, prevAcc, out);
+ sendUpdateAfter(curValue, out);
+ if (i != 0 && insertedId == -1) {
+ iterator.next();
+ }
+ }
+ i++;
+ }
+ }
+
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ // Update single row
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ } else if (equaliser.equals(curValue, input)) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ removeRowFromSameSortKeys(sortedKeyList, curKey, iterator, ids, out);
+ } else {
+ // Retract single record
+ RowData curAcc = accMapState.get(curKey.getLong(keyIdx));
+ sendUpdateBefore(input, curAcc, out);
Review Comment:
the row should be retracted with `delete` not with `update_before`.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
Review Comment:
I think we need to accumulate the current value before we can compare the accumulators.
So this call needs to go above line 328.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
Review Comment:
Is `inputKey.getLong(1)` hardcoded to `long` sortkeys?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ i++;
+ }
+ i = 0;
+ } else {
+ prevAcc = accMapState.get(ids.get(ids.size() - 2));
+ }
+
+ while (i < ids.size()) {
Review Comment:
use a `for`-loop here as well?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
Review Comment:
I think it would be better to have a single accumulator per sort key.
Then we would accumulate all rows with the same sort key and just update the accumulator once.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ i++;
+ }
+ i = 0;
+ } else {
+ prevAcc = accMapState.get(ids.get(ids.size() - 2));
+ }
+
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ if (ids.get(i) == insertedId) {
+ sendUpdate(curValue, RowKind.INSERT, out);
+ } else {
+ sendUpdateBefore(curValue, prevAcc, out);
+ sendUpdateAfter(curValue, out);
+ if (i != 0 && insertedId == -1) {
+ iterator.next();
+ }
Review Comment:
I find this call confusing. We are just advancing the iterator but we are not using it in this method.
Wouldn't it be better to advance it outside of this method by calling `next()` for `ids.size()` times?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ i++;
+ }
+ i = 0;
+ } else {
+ prevAcc = accMapState.get(ids.get(ids.size() - 2));
+ }
+
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ if (ids.get(i) == insertedId) {
+ sendUpdate(curValue, RowKind.INSERT, out);
+ } else {
+ sendUpdateBefore(curValue, prevAcc, out);
+ sendUpdateAfter(curValue, out);
+ if (i != 0 && insertedId == -1) {
+ iterator.next();
+ }
+ }
+ i++;
+ }
+ }
+
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
Review Comment:
Would it be easier to work with a manual list index instead of an iterator that is moved in two directions?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
Review Comment:
the name `sortedIds` is a bit misleading. The ids don't need to be sorted. They belong to the same sortkey.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
Review Comment:
The logic to early out is missing in the multi-row per sortkey case
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ i++;
+ }
+ i = 0;
+ } else {
+ prevAcc = accMapState.get(ids.get(ids.size() - 2));
+ }
+
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ if (ids.get(i) == insertedId) {
+ sendUpdate(curValue, RowKind.INSERT, out);
+ } else {
+ sendUpdateBefore(curValue, prevAcc, out);
+ sendUpdateAfter(curValue, out);
+ if (i != 0 && insertedId == -1) {
+ iterator.next();
+ }
+ }
+ i++;
+ }
+ }
+
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ // Update single row
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ } else if (equaliser.equals(curValue, input)) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ removeRowFromSameSortKeys(sortedKeyList, curKey, iterator, ids, out);
+ } else {
+ // Retract single record
+ RowData curAcc = accMapState.get(curKey.getLong(keyIdx));
+ sendUpdateBefore(input, curAcc, out);
+ iterator.remove();
+ removeFromState(sortedKeyList, ids, curKey.getLong(keyIdx), -1, curKey.getLong(1));
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ } else {
+ currFunction.setAccumulators(currFunction.createAccumulators());
+ }
+ }
+ isRetracted = true;
+ }
+ }
+ }
+
+ private void removeRowFromSameSortKeys(List<RowData> sortedKeyList, RowData curKey, ListIterator<RowData> iterator, List<Long> ids, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ int removeIndex = -1;
+ while (i < ids.size()) {
Review Comment:
reduce scope of `i`
```suggestion
for (int i = 0; i < ids.size(); i++) {
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ i++;
+ }
+ i = 0;
+ } else {
+ prevAcc = accMapState.get(ids.get(ids.size() - 2));
+ }
+
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ if (ids.get(i) == insertedId) {
+ sendUpdate(curValue, RowKind.INSERT, out);
+ } else {
+ sendUpdateBefore(curValue, prevAcc, out);
+ sendUpdateAfter(curValue, out);
+ if (i != 0 && insertedId == -1) {
+ iterator.next();
+ }
+ }
+ i++;
+ }
+ }
+
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ // Update single row
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ } else if (equaliser.equals(curValue, input)) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ removeRowFromSameSortKeys(sortedKeyList, curKey, iterator, ids, out);
+ } else {
+ // Retract single record
+ RowData curAcc = accMapState.get(curKey.getLong(keyIdx));
+ sendUpdateBefore(input, curAcc, out);
+ iterator.remove();
+ removeFromState(sortedKeyList, ids, curKey.getLong(keyIdx), -1, curKey.getLong(1));
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ } else {
+ currFunction.setAccumulators(currFunction.createAccumulators());
+ }
+ }
+ isRetracted = true;
+ }
+ }
+ }
+
+ private void removeRowFromSameSortKeys(List<RowData> sortedKeyList, RowData curKey, ListIterator<RowData> iterator, List<Long> ids, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ int removeIndex = -1;
+ while (i < ids.size()) {
+ if (ids.get(i) == curKey.getLong(keyIdx)) {
+ removeIndex = i;
+ break;
+ }
+ i++;
+ }
+
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.nextIndex() - 1 - i - 1);
+ RowData lastAcc = accMapState.get(ids.get(ids.size() - 1));
+ i = 0;
+ while (i < ids.size()) {
+ if (ids.get(i) == curKey.getLong(keyIdx)) {
+ // skip accumulating
+ } else {
+ RowData value = valueMapState.get(ids.get(i));
+ currFunction.accumulate(value);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ }
+ i++;
+ }
+
+ i = 0;
+ while (i < ids.size()) {
Review Comment:
```suggestion
for (int i = 0; i < ids.size(); i++) {
long id = ids.get(i);
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
Review Comment:
add some inline comments to make the logic of this method easier to understand
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ i++;
+ }
+ i = 0;
+ } else {
+ prevAcc = accMapState.get(ids.get(ids.size() - 2));
+ }
+
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ if (ids.get(i) == insertedId) {
+ sendUpdate(curValue, RowKind.INSERT, out);
+ } else {
+ sendUpdateBefore(curValue, prevAcc, out);
+ sendUpdateAfter(curValue, out);
+ if (i != 0 && insertedId == -1) {
+ iterator.next();
+ }
+ }
+ i++;
+ }
+ }
+
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ // Update single row
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ } else if (equaliser.equals(curValue, input)) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ removeRowFromSameSortKeys(sortedKeyList, curKey, iterator, ids, out);
+ } else {
+ // Retract single record
+ RowData curAcc = accMapState.get(curKey.getLong(keyIdx));
+ sendUpdateBefore(input, curAcc, out);
+ iterator.remove();
+ removeFromState(sortedKeyList, ids, curKey.getLong(keyIdx), -1, curKey.getLong(1));
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ } else {
+ currFunction.setAccumulators(currFunction.createAccumulators());
+ }
+ }
+ isRetracted = true;
+ }
+ }
+ }
+
+ private void removeRowFromSameSortKeys(List<RowData> sortedKeyList, RowData curKey, ListIterator<RowData> iterator, List<Long> ids, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ int removeIndex = -1;
+ while (i < ids.size()) {
+ if (ids.get(i) == curKey.getLong(keyIdx)) {
+ removeIndex = i;
+ break;
+ }
+ i++;
+ }
+
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.nextIndex() - 1 - i - 1);
Review Comment:
```suggestion
setAccumulatorOfPrevRow(sortedKeyList, iterator.nextIndex() - 1 - removeIndex - 1);
```
This is quite cryptic. A comment would help
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1919344202
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
Review Comment:
Yes temporarily. I have a TODO where the sortKey will be of Row type so that we can support multiple sort keys in future.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1920521952
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
Review Comment:
I think that will make the code also a bit easier to follow however, that will mean the aggregation function would need to support retract along with accumulate. Since we are storing the accumulated value for each row individually we don't need the retract method for the agg function.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1941380090
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
Review Comment:
RowData is an interface but all its implementations (eg: BinaryRowData) have an equals method implemented.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1941383870
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
Review Comment:
No, since the implementations of RowData have an equals method.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Add NonTimeRangeUnboundedPrecedingFunction for OverAggregate [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r2012509495
##########
flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecOverAggregate.java:
##########
@@ -309,73 +320,145 @@ private KeyedProcessFunction<RowData, RowData, RowData> createUnboundedOverProce
aggInputRowType,
JavaScalaConversionUtil.toScala(aggCalls),
new boolean[aggCalls.size()],
- false, // needRetraction
+ false, // needInputCount
true, // isStateBackendDataViews
true); // needDistinctInfo
LogicalType[] fieldTypes = inputRowType.getChildren().toArray(new LogicalType[0]);
- AggsHandlerCodeGenerator generator =
+
+ AggsHandlerCodeGenerator aggsGenerator =
new AggsHandlerCodeGenerator(
ctx,
relBuilder,
JavaScalaConversionUtil.toScala(Arrays.asList(fieldTypes)),
false); // copyInputField
- GeneratedAggsHandleFunction genAggsHandler =
- generator
+ aggsGenerator =
+ aggsGenerator
.needAccumulate()
// over agg code gen must pass the constants
- .withConstants(JavaScalaConversionUtil.toScala(constants))
- .generateAggsHandler("UnboundedOverAggregateHelper", aggInfoList);
+ .withConstants(JavaScalaConversionUtil.toScala(constants));
+
+ GeneratedAggsHandleFunction genAggsHandler =
+ aggsGenerator.generateAggsHandler("UnboundedOverAggregateHelper", aggInfoList);
LogicalType[] flattenAccTypes =
Arrays.stream(aggInfoList.getAccTypes())
.map(LogicalTypeDataTypeConverter::fromDataTypeToLogicalType)
.toArray(LogicalType[]::new);
- if (rowTimeIdx >= 0) {
- switch (unboundedOverVersion) {
- // Currently there is no migration path between first and second versions.
- case AbstractRowTimeUnboundedPrecedingOver.FIRST_OVER_VERSION:
- if (isRowsClause) {
- // ROWS unbounded over process function
- return new RowTimeRowsUnboundedPrecedingFunction<>(
+ switch (timeAttribute) {
+ case ROW_TIME:
+ final int rowTimeIdx = orderKeys[0];
+ switch (unboundedOverVersion) {
+ // Currently there is no migration path between first and second versions.
+ case AbstractRowTimeUnboundedPrecedingOver.FIRST_OVER_VERSION:
+ if (isRowsClause) {
+ // ROWS unbounded over process function
+ return new RowTimeRowsUnboundedPrecedingFunction<>(
+ config.getStateRetentionTime(),
+ TableConfigUtils.getMaxIdleStateRetentionTime(config),
+ genAggsHandler,
+ flattenAccTypes,
+ fieldTypes,
+ rowTimeIdx);
+ } else {
+ // RANGE unbounded over process function
+ return new RowTimeRangeUnboundedPrecedingFunction<>(
+ config.getStateRetentionTime(),
+ TableConfigUtils.getMaxIdleStateRetentionTime(config),
+ genAggsHandler,
+ flattenAccTypes,
+ fieldTypes,
+ rowTimeIdx);
+ }
+ case RowTimeUnboundedPrecedingOverFunctionV2.SECOND_OVER_VERSION:
+ return new RowTimeUnboundedPrecedingOverFunctionV2<>(
+ isRowsClause,
config.getStateRetentionTime(),
TableConfigUtils.getMaxIdleStateRetentionTime(config),
genAggsHandler,
flattenAccTypes,
fieldTypes,
rowTimeIdx);
- } else {
- // RANGE unbounded over process function
- return new RowTimeRangeUnboundedPrecedingFunction<>(
- config.getStateRetentionTime(),
- TableConfigUtils.getMaxIdleStateRetentionTime(config),
- genAggsHandler,
- flattenAccTypes,
- fieldTypes,
- rowTimeIdx);
- }
- case RowTimeUnboundedPrecedingOverFunctionV2.SECOND_OVER_VERSION:
- return new RowTimeUnboundedPrecedingOverFunctionV2<>(
- isRowsClause,
- config.getStateRetentionTime(),
- TableConfigUtils.getMaxIdleStateRetentionTime(config),
- genAggsHandler,
- flattenAccTypes,
- fieldTypes,
- rowTimeIdx);
- default:
- throw new UnsupportedOperationException(
- "Unsupported unbounded over version: "
- + unboundedOverVersion
- + ". Valid versions are 1 and 2.");
- }
- } else {
- return new ProcTimeUnboundedPrecedingFunction<>(
- StateConfigUtil.createTtlConfig(config.getStateRetentionTime()),
- genAggsHandler,
- flattenAccTypes);
+ default:
+ throw new UnsupportedOperationException(
+ "Unsupported unbounded over version: "
+ + unboundedOverVersion
+ + ". Valid versions are 1 and 2.");
+ }
+ case PROC_TIME:
+ return new ProcTimeUnboundedPrecedingFunction<>(
+ StateConfigUtil.createTtlConfig(config.getStateRetentionTime()),
+ genAggsHandler,
+ flattenAccTypes);
+ case NON_TIME:
+ if (isRowsClause) {
+ // Non-Time Rows Unbounded Preceding Function
+ throw new TableException(
+ "Non-Time Rows Unbounded Preceding Function not supported yet.");
Review Comment:
Fixed as suggested
##########
flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecOverAggregate.java:
##########
@@ -439,33 +524,44 @@ private KeyedProcessFunction<RowData, RowData, RowData> createBoundedOverProcess
.map(LogicalTypeDataTypeConverter::fromDataTypeToLogicalType)
.toArray(LogicalType[]::new);
- if (rowTimeIdx >= 0) {
- if (isRowsClause) {
- return new RowTimeRowsBoundedPrecedingFunction<>(
- config.getStateRetentionTime(),
- TableConfigUtils.getMaxIdleStateRetentionTime(config),
- genAggsHandler,
- flattenAccTypes,
- fieldTypes,
- precedingOffset,
- rowTimeIdx);
- } else {
- return new RowTimeRangeBoundedPrecedingFunction<>(
- genAggsHandler, flattenAccTypes, fieldTypes, precedingOffset, rowTimeIdx);
- }
- } else {
- if (isRowsClause) {
- return new ProcTimeRowsBoundedPrecedingFunction<>(
- config.getStateRetentionTime(),
- TableConfigUtils.getMaxIdleStateRetentionTime(config),
- genAggsHandler,
- flattenAccTypes,
- fieldTypes,
- precedingOffset);
- } else {
- return new ProcTimeRangeBoundedPrecedingFunction<>(
- genAggsHandler, flattenAccTypes, fieldTypes, precedingOffset);
- }
+ switch (timeAttribute) {
+ case ROW_TIME:
+ final int rowTimeIdx = orderKeys[0];
+ if (isRowsClause) {
+ return new RowTimeRowsBoundedPrecedingFunction<>(
+ config.getStateRetentionTime(),
+ TableConfigUtils.getMaxIdleStateRetentionTime(config),
+ genAggsHandler,
+ flattenAccTypes,
+ fieldTypes,
+ precedingOffset,
+ rowTimeIdx);
+ } else {
+ return new RowTimeRangeBoundedPrecedingFunction<>(
+ genAggsHandler,
+ flattenAccTypes,
+ fieldTypes,
+ precedingOffset,
+ rowTimeIdx);
+ }
+ case PROC_TIME:
+ if (isRowsClause) {
+ return new ProcTimeRowsBoundedPrecedingFunction<>(
+ config.getStateRetentionTime(),
+ TableConfigUtils.getMaxIdleStateRetentionTime(config),
+ genAggsHandler,
+ flattenAccTypes,
+ fieldTypes,
+ precedingOffset);
+ } else {
+ return new ProcTimeRangeBoundedPrecedingFunction<>(
+ genAggsHandler, flattenAccTypes, fieldTypes, precedingOffset);
+ }
+ case NON_TIME:
+ throw new TableException(
+ "Non-time attribute sort is not supported for bounded over aggregate");
+ default:
+ throw new TableException("Unsupported bounded operation");
Review Comment:
Fixed
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1907586826
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ insertIntoSortedListOptimized(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ removeFromSortedListOptimized(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
Review Comment:
The new implementation stores accumulators as suggested.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1908080757
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
Review Comment:
Moved the logic to `updateRow()`
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
Review Comment:
Moved the logic to `updateRow()`
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928126475
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
Review Comment:
Reimplemented and this function no longer exists.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928129854
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
Review Comment:
New implementation uses for loops
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ i++;
+ }
+ i = 0;
+ } else {
+ prevAcc = accMapState.get(ids.get(ids.size() - 2));
+ }
+
+ while (i < ids.size()) {
Review Comment:
Yes
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928136028
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
Review Comment:
Stale hence resolving.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928131539
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
Review Comment:
Yes, the new implementation takes care of this.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928136710
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+
+ currFunction.accumulate(curValue);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ RowData curAcc = accMapState.get(curKey.getLong(keyIdx));
+ currFunction.setAccumulators(curAcc);
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+
+ valueMapState.remove(curKey.getLong(keyIdx));
+ accMapState.remove(curKey.getLong(keyIdx));
+ if (iterator.hasPrevious()) {
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ RowData prevAcc = accMapState.get(prev.getLong(keyIdx));
+ currFunction.setAccumulators(prevAcc);
Review Comment:
Stale hence resolving
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1946983985
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
Review Comment:
Update javadoc
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
Review Comment:
Fixed
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
Review Comment:
Done
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
Review Comment:
Buffered the last fetched row to avoid reading it from valueMapState twice
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+
+ // update accumulated state for sortKey
+ accMapState.put(curSortKey, currAcc);
+ }
+ }
+
+ private void sendUpdateForChangedRow(
+ Collector<RowData> out,
+ RowKind rowKind,
+ RowData changedRow,
+ RowData prevAggValue,
+ RowData currAggValue) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, changedRow, prevAggValue);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, changedRow, rowKind, currAggValue);
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(
+ Collector<RowData> out, RowData value, RowKind rowKind, RowData currAggValue) {
+ output.setRowKind(rowKind);
+ output.replace(value, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAggValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching delRow from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param delRow
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
+ int i = 0;
+ boolean sortKeyFound = false;
+ delRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(delRow);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ sortKeyFound = true;
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, delRow);
+ if (removeIndex == -1) {
+ LOG.info(
+ "Could not find matching row to remove. Missing id from sortKey ids list.");
+ numOfIdsNotFound.inc();
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ RowKind.DELETE,
+ delRow,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(
+ sortedList, ids.size(), deletedId, curSortKey, aggFuncs.getAccumulators());
+ break;
+ }
+ }
+
+ if (!sortKeyFound) {
+ LOG.debug("Could not find matching sort key. Skipping delete.");
+ numOfSortKeysNotFound.inc();
+ }
+
+ processRemainingElements(sortedList, i, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids without the id that will be
+ * removed.
+ *
+ * @param ids
+ * @param delRow
+ * @return the index position of the id that should be removed
+ * @throws Exception
+ */
+ private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData delRow) throws Exception {
+ int removeIndex = -1;
+ for (int j = 0; j < ids.size(); j++) {
+ RowData curValue = valueMapState.get(ids.get(j));
+ if (valueEqualiser.equals(curValue, delRow)) {
+ removeIndex = j;
+ } else {
+ aggFuncs.accumulate(curValue);
+ }
+ }
+ return removeIndex;
+ }
+
+ /**
+ * Remove the ith element from the sortedList if the list of ids is empty and return the ith
+ * location OR update the sorted list with the list of ids and return the next location of the
+ * sortedList.
+ *
+ * @param sortedList
+ * @param idx
+ * @param ids
+ * @param curSortKey
+ * @return
+ */
+ private int removeIdFromSortedList(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int idx,
+ List<Long> ids,
+ RowData curSortKey) {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ // Update sortedList without the removed id
+ sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
+ idx++;
+ }
+ }
+ return idx;
+ }
+
+ private void removeFromState(
Review Comment:
Removed method and inlined into `removeFromSortedList()`
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
Review Comment:
Fixed
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
Review Comment:
Updated comment
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
Review Comment:
Removed the method and inlined in `insertIntoSortedList()`
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
Review Comment:
Done
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
Review Comment:
Fixed
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+
+ // update accumulated state for sortKey
+ accMapState.put(curSortKey, currAcc);
+ }
+ }
+
+ private void sendUpdateForChangedRow(
+ Collector<RowData> out,
+ RowKind rowKind,
+ RowData changedRow,
+ RowData prevAggValue,
+ RowData currAggValue) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, changedRow, prevAggValue);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, changedRow, rowKind, currAggValue);
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(
+ Collector<RowData> out, RowData value, RowKind rowKind, RowData currAggValue) {
+ output.setRowKind(rowKind);
+ output.replace(value, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAggValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching delRow from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param delRow
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
+ int i = 0;
+ boolean sortKeyFound = false;
+ delRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(delRow);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (; i < sortedList.size(); i++) {
Review Comment:
Renamed to `findIndexOfSortKey` and reused it in `removedFromSortedList()`
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+
+ // update accumulated state for sortKey
+ accMapState.put(curSortKey, currAcc);
+ }
+ }
+
+ private void sendUpdateForChangedRow(
+ Collector<RowData> out,
+ RowKind rowKind,
+ RowData changedRow,
+ RowData prevAggValue,
+ RowData currAggValue) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, changedRow, prevAggValue);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, changedRow, rowKind, currAggValue);
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(
+ Collector<RowData> out, RowData value, RowKind rowKind, RowData currAggValue) {
+ output.setRowKind(rowKind);
+ output.replace(value, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAggValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching delRow from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param delRow
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
+ int i = 0;
+ boolean sortKeyFound = false;
+ delRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(delRow);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ sortKeyFound = true;
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, delRow);
+ if (removeIndex == -1) {
+ LOG.info(
+ "Could not find matching row to remove. Missing id from sortKey ids list.");
+ numOfIdsNotFound.inc();
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ RowKind.DELETE,
+ delRow,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(
+ sortedList, ids.size(), deletedId, curSortKey, aggFuncs.getAccumulators());
+ break;
+ }
+ }
+
+ if (!sortKeyFound) {
+ LOG.debug("Could not find matching sort key. Skipping delete.");
+ numOfSortKeysNotFound.inc();
Review Comment:
Fixed
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1908078483
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
Review Comment:
Moved to helper function
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1908082403
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
Review Comment:
This is not used and removed.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1907588405
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ insertIntoSortedListOptimized(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ removeFromSortedListOptimized(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
Review Comment:
This implementation is stale. The new implementation takes care of this condition.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928130617
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
Review Comment:
Yes, the new implementation uses collect* methods which are lot easier to digest
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928133258
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
Review Comment:
Reimplemented with accState per sortKey and also updated the sortedList type
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928126027
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
Review Comment:
Changed the implementation to use sortKey as a RowData which can fit arbitrary no of data types within it based on the ORDER BY clause.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1907588797
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
Review Comment:
The new implementation persists accumulators.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1919336795
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
Review Comment:
Yes, thats right. Here is the generated code:
```
public class SortComparator$0 implements org.apache.flink.table.runtime.generated.RecordComparator {
private final Object[] references;
public SortComparator$0(Object[] references) {
this.references = references;
}
@Override
public int compare(org.apache.flink.table.data.RowData o1, org.apache.flink.table.data.RowData o2) {
boolean isNullA$2 = o1.isNullAt(1);
boolean isNullB$4 = o2.isNullAt(1);
if (isNullA$2 && isNullB$4) {
// Continue to compare the next element
} else if (isNullA$2) {
return -1;
} else if (isNullB$4) {
return 1;
} else {
long fieldA$1 = o1.getLong(1);
long fieldB$3 = o2.getLong(1);
int comp$5 = (fieldA$1 > fieldB$3 ? 1 : fieldA$1 < fieldB$3 ? -1 : 0);
if (comp$5 != 0) {
return comp$5;
}
}
return 0;
}
}
```
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Add NonTimeRangeUnboundedPrecedingFunction for OverAggregate [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r2012507962
##########
docs/content/docs/dev/table/sql/queries/over-agg.md:
##########
@@ -56,7 +56,7 @@ You can define multiple `OVER` window aggregates in a `SELECT` clause. However,
### ORDER BY
-`OVER` windows are defined on an ordered sequence of rows. Since tables do not have an inherent order, the `ORDER BY` clause is mandatory. For streaming queries, Flink currently only supports `OVER` windows that are defined with an ascending [time attributes]({{< ref "docs/dev/table/concepts/time_attributes" >}}) order. Additional orderings are not supported.
+`OVER` windows are defined on an ordered sequence of rows. Since tables do not have an inherent order, the `ORDER BY` clause is mandatory. For streaming queries, Flink currently supports `OVER` windows that are defined with an ascending [time attributes]({{< ref "docs/dev/table/concepts/time_attributes" >}}) order or ascending non-time attributes. Additional orderings are not supported.
Review Comment:
Fixed as suggested.
##########
docs/content/docs/dev/table/sql/queries/over-agg.md:
##########
@@ -70,11 +70,20 @@ There are two options to define the range, `ROWS` intervals and `RANGE` interval
#### RANGE intervals
-A `RANGE` interval is defined on the values of the ORDER BY column, which is in case of Flink always a time attribute. The following RANGE interval defines that all rows with a time attribute of at most 30 minutes less than the current row are included in the aggregate.
+A `RANGE` interval is defined on the values of the ORDER BY column, which is in case of Flink is either a time or non-time attribute.
Review Comment:
Fixed
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1964080867
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching input from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ int i;
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, input);
+ if (removeIndex == -1) {
+ // TODO: can this ever happen? may be during/after state cleaning?
Review Comment:
Added a log and a metric when data is not returned by state.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "fhueske (via GitHub)" <gi...@apache.org>.
fhueske commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1908354102
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
Review Comment:
That's correct, but the problem are records with identical timestamps and that could also happen for the event-time column.
See the example above which uses `OVER (ORDER BY num)`. For all records with `num = `` the same sum is returned (`6`).
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Add NonTimeRangeUnboundedPrecedingFunction for OverAggregate [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r2012510561
##########
flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/OverAggregateNDUTest.java:
##########
@@ -0,0 +1,129 @@
+/*
+ * 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.flink.table.planner.plan.nodes.exec.stream;
+
+import org.apache.flink.table.api.TableConfig;
+import org.apache.flink.table.api.TableEnvironment;
+import org.apache.flink.table.api.TableException;
+import org.apache.flink.table.api.config.OptimizerConfigOptions;
+import org.apache.flink.table.api.config.OptimizerConfigOptions.NonDeterministicUpdateStrategy;
+import org.apache.flink.table.planner.utils.StreamTableTestUtil;
+import org.apache.flink.table.planner.utils.TableTestBase;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/** Test NDU errors for over aggregate. */
+class OverAggregateNDUTest extends TableTestBase {
Review Comment:
Moved the tests to `NonDeterministicUpdateAnalyzerTest `
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Add NonTimeRangeUnboundedPrecedingFunction for OverAggregate [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r2012510187
##########
flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalOverAggregate.scala:
##########
@@ -27,9 +29,12 @@ import org.apache.calcite.plan.{RelOptCluster, RelTraitSet}
import org.apache.calcite.rel.`type`.RelDataType
import org.apache.calcite.rel.RelNode
import org.apache.calcite.rel.core.Window
+import org.apache.calcite.util.ImmutableBitSet
Review Comment:
Removed unused imports
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Add NonTimeRangeUnboundedPrecedingFunction for OverAggregate [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r2012512086
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,688 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.StateTtlConfig;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+import static org.apache.flink.table.runtime.util.StateConfigUtil.createTtlConfig;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
Review Comment:
Added a java doc describing high level summary and limitations
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928136852
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
Review Comment:
State hence resolving
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
Review Comment:
Stale hence resolving
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928135793
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
Review Comment:
See `addToState` and `removeFromState` methods
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928132811
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
Review Comment:
The new implementation has a single accumulator per sort key.
Additionally it also maintains a sortedList which contains tuples of (SortKey, List<Long Ids>)
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928131810
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
Review Comment:
Yes, fixed in new implementation.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1909223823
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
Review Comment:
This seems to be correctly handled for `RowTimeRangeUnboundedPrecedingFunction` i.e. all same timestamp(i.e. the same sortKey) should have the same aggregation value
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "fhueske (via GitHub)" <gi...@apache.org>.
fhueske commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1948551249
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+
+ // update accumulated state for sortKey
+ accMapState.put(curSortKey, currAcc);
+ }
+ }
+
+ private void sendUpdateForChangedRow(
+ Collector<RowData> out,
+ RowKind rowKind,
+ RowData changedRow,
+ RowData prevAggValue,
+ RowData currAggValue) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, changedRow, prevAggValue);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, changedRow, rowKind, currAggValue);
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(
+ Collector<RowData> out, RowData value, RowKind rowKind, RowData currAggValue) {
+ output.setRowKind(rowKind);
+ output.replace(value, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAggValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching delRow from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param delRow
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
+ int i = 0;
+ boolean sortKeyFound = false;
+ delRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(delRow);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ sortKeyFound = true;
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, delRow);
+ if (removeIndex == -1) {
+ LOG.info(
+ "Could not find matching row to remove. Missing id from sortKey ids list.");
+ numOfIdsNotFound.inc();
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ RowKind.DELETE,
+ delRow,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(
+ sortedList, ids.size(), deletedId, curSortKey, aggFuncs.getAccumulators());
+ break;
+ }
+ }
+
+ if (!sortKeyFound) {
+ LOG.debug("Could not find matching sort key. Skipping delete.");
+ numOfSortKeysNotFound.inc();
+ }
+
+ processRemainingElements(sortedList, i, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids without the id that will be
+ * removed.
+ *
+ * @param ids
+ * @param delRow
+ * @return the index position of the id that should be removed
+ * @throws Exception
+ */
+ private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData delRow) throws Exception {
+ int removeIndex = -1;
+ for (int j = 0; j < ids.size(); j++) {
+ RowData curValue = valueMapState.get(ids.get(j));
+ if (valueEqualiser.equals(curValue, delRow)) {
+ removeIndex = j;
+ } else {
+ aggFuncs.accumulate(curValue);
+ }
+ }
+ return removeIndex;
+ }
+
+ /**
+ * Remove the ith element from the sortedList if the list of ids is empty and return the ith
+ * location OR update the sorted list with the list of ids and return the next location of the
+ * sortedList.
+ *
+ * @param sortedList
+ * @param idx
+ * @param ids
+ * @param curSortKey
+ * @return
+ */
+ private int removeIdFromSortedList(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int idx,
+ List<Long> ids,
+ RowData curSortKey) {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ // Update sortedList without the removed id
+ sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
+ idx++;
+ }
Review Comment:
The "problem" is the nesting of the two if statements with identical conditions.
We would never get into the `else` branch of the outer `if` statement, if `ids.isEmpty()` was true?
Or am I missing something?
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928125261
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
Review Comment:
In the new implementation standardized this i.e. while storing use `currFunction.getAccumulators()` and while emitting updates use `currFunction.getValue()`
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1940347668
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
Review Comment:
Yes, Im working on cleaning these up for consistency. However, I have an AVG test which is passing even with these mixed up usage.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1945487565
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,692 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ aggFuncs = generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ aggFuncs.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+
+ // metrics
+ this.numOfIdsNotFound =
+ getRuntimeContext().getMetricGroup().counter(IDS_NOT_FOUND_METRIC_NAME);
+ this.numOfSortKeysNotFound =
+ getRuntimeContext().getMetricGroup().counter(SORT_KEYS_NOT_FOUND_METRIC_NAME);
+ }
+
+ /**
+ * Puts an element from the input stream into state or removes it from state if the input is a
+ * retraction. Emits the aggregated value for the newly inserted element and updates all results
+ * that are affected by the added or removed row.
+ * Emits the same aggregated value for all elements with the same sortKey to comply with the
+ * sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer for the current key
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ aggFuncs.resetAccumulators();
+ aggFuncs.cleanup();
+ }
+
+ /**
+ * Adds a new element(insRow) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>>. Extracts the inputSortKey from the insRow and compares it with every element
+ * in the sortedList. If a sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList, otherwise find the right position in the
+ * sortedList and add a new entry in the sortedList.
+ * After the insRow is successfully inserted, an INSERT/UPDATE_AFTER
+ * event is emitted for the newly inserted element, and for all subsequent elements an
+ * UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly aggregated
+ * values. Some updates are skipped if the previously accumulated value is the same as the newly
+ * accumulated value to save on network bandwidth.
+ *
+ * @param insRow The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = insRow.getRowKind();
+ insRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(insRow);
+ Tuple2<Integer, Boolean> indexForInsertOrUpdate =
+ findIndexForInsertOrUpdate(sortedList, inputSortKey);
+ boolean isInsert = indexForInsertOrUpdate.f1;
+ int index = indexForInsertOrUpdate.f0;
+ if (isInsert) {
+ if (index == -1) {
+ // Insert at the end of the sortedList
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ index = sortedList.size() - 1;
+ } else {
+ // Insert at position i of the sortedList
+ sortedList.add(index, new Tuple2<>(inputSortKey, List.of(id)));
+ }
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ aggFuncs.accumulate(insRow);
+ collectInsertOrUpdateAfter(out, insRow, origRowKind, aggFuncs.getValue());
+ } else {
+ // Update at position i
+ List<Long> ids = new ArrayList<>(sortedList.get(index).f1);
+ ids.add(id);
+ sortedList.set(index, new Tuple2<>(inputSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, index - 1);
+ reAccumulateIdsAfterInsert(aggFuncs.getAccumulators(), ids, insRow);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(inputSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ origRowKind,
+ insRow,
+ out);
+ }
+
+ addToState(id, insRow, inputSortKey, aggFuncs.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, index + 1, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = Long.MIN_VALUE;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Returns the position of the index where the inputRow must be inserted or updated. If a
+ * suitable position is not found, return -1 to be inserted at the end of the list.
+ *
+ * @param sortedList
+ * @param inputSortKey
+ * @return Tuple2<Integer, Boolean>
+ */
+ private Tuple2<Integer, Boolean> findIndexForInsertOrUpdate(
+ List<Tuple2<RowData, List<Long>>> sortedList, RowData inputSortKey) {
+ // TODO: Optimize by using Binary Search
+ for (int i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Found inputSortKey
+ return new Tuple2<>(i, false);
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Found curSortKey which is greater than inputSortKey
+ return new Tuple2<>(i, true);
+ }
+ }
+ // Return not found
+ return new Tuple2<>(-1, true);
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ aggFuncs.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ aggFuncs.createAccumulators();
+ } else {
+ aggFuncs.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param currAcc
+ * @param ids
+ * @param insRow
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(RowData currAcc, List<Long> ids, RowData insRow)
+ throws Exception {
+ aggFuncs.setAccumulators(currAcc);
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size() - 1; j++) {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+ // Update acc for newly inserted row
+ aggFuncs.accumulate(insRow);
+ }
+
+ private RowData setAccumulatorAndGetValue(RowData accumulator) throws Exception {
+ aggFuncs.setAccumulators(accumulator);
+ return aggFuncs.getValue();
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idxOfChangedRow location either emits a DELETE
+ * or INSERT/UPDATE_AFTER, depending on whether the id is being added or removed from ids. All
+ * other ids except the id at position idxOfChangedRow emit old and new aggregated values in the
+ * form of UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idxOfChangedRow
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param changedRow
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idxOfChangedRow,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData changedRow,
+ Collector<RowData> out)
+ throws Exception {
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+ RowData currAggValue = setAccumulatorAndGetValue(currAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Only send update for changed row i.e. either INSERT or DELETE
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug("Prev accumulator is same as curr accumulator. Skipping further updates.");
+ return;
+ }
+
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idxOfChangedRow) {
+ sendUpdateForChangedRow(out, rowKind, changedRow, prevAggValue, currAggValue);
+ } else {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param currAcc
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int startPos,
+ RowData currAcc,
+ Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+
+ aggFuncs.setAccumulators(currAcc);
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ aggFuncs.accumulate(value);
+ }
+
+ // Update currAcc with the updated aggFunc
+ currAcc = aggFuncs.getAccumulators();
+ RowData currAggValue = aggFuncs.getValue();
+
+ // Get previous accumulator and previously aggregated value
+ RowData prevAcc = accMapState.get(curSortKey);
+ RowData prevAggValue = setAccumulatorAndGetValue(prevAcc);
+
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.debug(
+ "Prev accumulator is same as curr accumulator. Skipping further updates.");
+ break;
+ }
+
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAggValue);
+ collectUpdateAfter(out, value, currAggValue);
+ }
+
+ // update accumulated state for sortKey
+ accMapState.put(curSortKey, currAcc);
+ }
+ }
+
+ private void sendUpdateForChangedRow(
+ Collector<RowData> out,
+ RowKind rowKind,
+ RowData changedRow,
+ RowData prevAggValue,
+ RowData currAggValue) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, changedRow, prevAggValue);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, changedRow, rowKind, currAggValue);
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(
+ Collector<RowData> out, RowData value, RowKind rowKind, RowData currAggValue) {
+ output.setRowKind(rowKind);
+ output.replace(value, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAggValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAggValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAggValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching delRow from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param delRow
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
+ int i = 0;
+ boolean sortKeyFound = false;
+ delRow.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(delRow);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ sortKeyFound = true;
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, delRow);
+ if (removeIndex == -1) {
+ LOG.info(
+ "Could not find matching row to remove. Missing id from sortKey ids list.");
+ numOfIdsNotFound.inc();
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey), // prevAcc
+ aggFuncs.getAccumulators(), // currAcc
+ RowKind.DELETE,
+ delRow,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(
+ sortedList, ids.size(), deletedId, curSortKey, aggFuncs.getAccumulators());
+ break;
+ }
+ }
+
+ if (!sortKeyFound) {
+ LOG.debug("Could not find matching sort key. Skipping delete.");
+ numOfSortKeysNotFound.inc();
+ }
+
+ processRemainingElements(sortedList, i, aggFuncs.getAccumulators(), out);
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids without the id that will be
+ * removed.
+ *
+ * @param ids
+ * @param delRow
+ * @return the index position of the id that should be removed
+ * @throws Exception
+ */
+ private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData delRow) throws Exception {
+ int removeIndex = -1;
+ for (int j = 0; j < ids.size(); j++) {
+ RowData curValue = valueMapState.get(ids.get(j));
+ if (valueEqualiser.equals(curValue, delRow)) {
+ removeIndex = j;
+ } else {
+ aggFuncs.accumulate(curValue);
+ }
+ }
+ return removeIndex;
+ }
+
+ /**
+ * Remove the ith element from the sortedList if the list of ids is empty and return the ith
+ * location OR update the sorted list with the list of ids and return the next location of the
+ * sortedList.
+ *
+ * @param sortedList
+ * @param idx
+ * @param ids
+ * @param curSortKey
+ * @return
+ */
+ private int removeIdFromSortedList(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int idx,
+ List<Long> ids,
+ RowData curSortKey) {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ // Update sortedList without the removed id
+ sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
+ idx++;
+ }
Review Comment:
If we remove the `if` condition, wont this create holes in the sortedList?
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1942243498
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,578 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
Review Comment:
Updated comment
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
Review Comment:
Updated javadoc
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
Review Comment:
Extracted the logic to find the index to a function.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
Review Comment:
Yes, makes sense. Fixed.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
Review Comment:
Fixed
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
Review Comment:
Fixed
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching input from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ int i;
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, input);
+ if (removeIndex == -1) {
+ // TODO: can this ever happen? may be during/after state cleaning?
+ LOG.info("Could not find id to remove");
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey),
+ currFunction.getAccumulators(),
+ RowKind.DELETE,
+ input,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(sortedList, deletedId, curSortKey);
+ break;
+ }
+ }
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids without the id that will be
+ * removed.
+ *
+ * @param ids
+ * @param input
+ * @return the index position of the id that should be removed
+ * @throws Exception
+ */
+ private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData input) throws Exception {
Review Comment:
Renamed
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching input from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ int i;
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, input);
+ if (removeIndex == -1) {
+ // TODO: can this ever happen? may be during/after state cleaning?
+ LOG.info("Could not find id to remove");
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey),
+ currFunction.getAccumulators(),
+ RowKind.DELETE,
+ input,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(sortedList, deletedId, curSortKey);
+ break;
+ }
+ }
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids without the id that will be
+ * removed.
+ *
+ * @param ids
+ * @param input
+ * @return the index position of the id that should be removed
+ * @throws Exception
+ */
+ private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData input) throws Exception {
+ int removeIndex = -1;
+ for (int j = 0; j < ids.size(); j++) {
+ RowData curValue = valueMapState.get(ids.get(j));
+ if (valueEqualiser.equals(curValue, input)) {
+ removeIndex = j;
+ } else {
+ currFunction.accumulate(curValue);
+ }
+ }
+ return removeIndex;
+ }
+
+ /**
+ * Remove the ith element from the sortedList if the list of ids is empty and return the ith
+ * location OR update the sorted list with the list of ids and return the next location of the
+ * sortedList.
+ *
+ * @param sortedList
+ * @param idx
+ * @param ids
+ * @param curSortKey
+ * @return
+ */
+ private int removeIdFromSortedList(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int idx,
+ List<Long> ids,
+ RowData curSortKey) {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ // Update sortedList without the removed id
+ sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
+ idx++;
+ }
+ return idx;
+ }
+
+ private void removeFromState(
+ List<Tuple2<RowData, List<Long>>> sortedList, Long id, RowData sortKey)
+ throws Exception {
+ valueMapState.remove(id);
+ accMapState.put(sortKey, currFunction.getAccumulators());
Review Comment:
Also added a test in `NonTimeRangeUnboundedPrecedingFunctionTest` to validate state content/size.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
Review Comment:
Fixed javadoc
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
Review Comment:
Updated comment
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
Review Comment:
Updated javadoc
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
Review Comment:
After refactoring, the following 2 rules are being followed:
1. While storing and comparing/equality, the entire accumulator is stored or compared.
2. While emitting out rows, the aggregated value is being emitted.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
Review Comment:
Refactored
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
Review Comment:
Renamed
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
Review Comment:
Will do a separate commit for ttl related stuff.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
Review Comment:
Renamed to aggFuncs
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
Review Comment:
Refactored
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
Review Comment:
Renamed
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
Review Comment:
Yes, for now all tests are using updateBefore. Will fix this as a separate commit. Added a TODO so that I don't miss this.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
Review Comment:
Fixed
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
Review Comment:
Yes, makes sense. Refactored accordingly.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
Review Comment:
Yes. Refactored.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
Review Comment:
Fixed by passing the aggregated value
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
Review Comment:
Changed to debug.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
Review Comment:
Fixed
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
Review Comment:
Changed to debug
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
Review Comment:
Refactored to load the acc into the function and compute the new aggregated value
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching input from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
Review Comment:
Renamed
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
Review Comment:
Moved the logic to find the index for insert/update. This allowed to reuse the common code.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching input from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ int i;
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, input);
+ if (removeIndex == -1) {
+ // TODO: can this ever happen? may be during/after state cleaning?
+ LOG.info("Could not find id to remove");
Review Comment:
Added a Counter metric - `numOfIdsNotFound`
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
Review Comment:
Renamed input to insRow
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching input from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ int i;
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, input);
+ if (removeIndex == -1) {
+ // TODO: can this ever happen? may be during/after state cleaning?
+ LOG.info("Could not find id to remove");
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey),
+ currFunction.getAccumulators(),
+ RowKind.DELETE,
+ input,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(sortedList, deletedId, curSortKey);
+ break;
+ }
+ }
Review Comment:
Added a log line plus a Counter metric - `numOfSortKeysNotFound`
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching input from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ int i;
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, input);
+ if (removeIndex == -1) {
+ // TODO: can this ever happen? may be during/after state cleaning?
+ LOG.info("Could not find id to remove");
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey),
+ currFunction.getAccumulators(),
+ RowKind.DELETE,
+ input,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(sortedList, deletedId, curSortKey);
+ break;
+ }
+ }
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids without the id that will be
+ * removed.
+ *
+ * @param ids
+ * @param input
+ * @return the index position of the id that should be removed
+ * @throws Exception
+ */
+ private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData input) throws Exception {
+ int removeIndex = -1;
+ for (int j = 0; j < ids.size(); j++) {
+ RowData curValue = valueMapState.get(ids.get(j));
+ if (valueEqualiser.equals(curValue, input)) {
+ removeIndex = j;
+ } else {
+ currFunction.accumulate(curValue);
+ }
+ }
+ return removeIndex;
+ }
+
+ /**
+ * Remove the ith element from the sortedList if the list of ids is empty and return the ith
+ * location OR update the sorted list with the list of ids and return the next location of the
+ * sortedList.
+ *
+ * @param sortedList
+ * @param idx
+ * @param ids
+ * @param curSortKey
+ * @return
+ */
+ private int removeIdFromSortedList(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int idx,
+ List<Long> ids,
+ RowData curSortKey) {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ // Update sortedList without the removed id
+ sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
+ idx++;
+ }
+ return idx;
+ }
+
+ private void removeFromState(
+ List<Tuple2<RowData, List<Long>>> sortedList, Long id, RowData sortKey)
+ throws Exception {
+ valueMapState.remove(id);
+ accMapState.put(sortKey, currFunction.getAccumulators());
Review Comment:
Fixed
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Add NonTimeRangeUnboundedPrecedingFunction for OverAggregate [flink]
Posted by "twalthr (via GitHub)" <gi...@apache.org>.
twalthr commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r2007913786
##########
flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecOverAggregate.java:
##########
@@ -183,15 +190,15 @@ protected Transformation<RowData> translateToPlanInternal(
final int orderKey = orderKeys[0];
final LogicalType orderKeyType = inputRowType.getFields().get(orderKey).getType();
- // check time field && identify window rowtime attribute
- final int rowTimeIdx;
+ // check time field && identify window time attribute
+ TimeAttribute timeAttribute;
if (isRowtimeAttribute(orderKeyType)) {
- rowTimeIdx = orderKey;
+ timeAttribute = TimeAttribute.ROW_TIME;
} else if (isProctimeAttribute(orderKeyType)) {
- rowTimeIdx = -1;
+ timeAttribute = TimeAttribute.PROC_TIME;
} else {
- throw new TableException(
- "OVER windows' ordering in stream mode must be defined on a time attribute.");
+ timeAttribute = TimeAttribute.NON_TIME;
+ LOG.info("Non-time attribute window detected");
Review Comment:
It's uncommon that we log this tiny properties within the planner. I recommend to drop it.
##########
docs/content/docs/dev/table/sql/queries/over-agg.md:
##########
@@ -70,11 +70,20 @@ There are two options to define the range, `ROWS` intervals and `RANGE` interval
#### RANGE intervals
-A `RANGE` interval is defined on the values of the ORDER BY column, which is in case of Flink always a time attribute. The following RANGE interval defines that all rows with a time attribute of at most 30 minutes less than the current row are included in the aggregate.
+A `RANGE` interval is defined on the values of the ORDER BY column, which is in case of Flink is either a time or non-time attribute.
+
+The following RANGE interval defines that all rows with a time attribute of at most 30 minutes less than the current row are included in the aggregate.
```sql
RANGE BETWEEN INTERVAL '30' MINUTE PRECEDING AND CURRENT ROW
```
+The following RANGE interval defines that all rows with a non-time attribute of unbounded rows preceding the current row are included in the aggregate.
+```sql
+RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
+```
+
Review Comment:
remove empty lines
##########
flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/OverWindowRestoreTest.java:
##########
@@ -36,7 +36,7 @@ public OverWindowRestoreTest() {
@Override
protected Stream<String> getSavepointPaths(
TableTestProgram program, ExecNodeMetadata metadata) {
- if (metadata.version() == 1) {
+ if (metadata.version() == 1 && program.equals(OverWindowTestPrograms.LAG_OVER_FUNCTION)) {
Review Comment:
can you add a comment for this change? just looking at code this seems odd
##########
flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecOverAggregate.java:
##########
@@ -309,73 +320,145 @@ private KeyedProcessFunction<RowData, RowData, RowData> createUnboundedOverProce
aggInputRowType,
JavaScalaConversionUtil.toScala(aggCalls),
new boolean[aggCalls.size()],
- false, // needRetraction
+ false, // needInputCount
true, // isStateBackendDataViews
true); // needDistinctInfo
LogicalType[] fieldTypes = inputRowType.getChildren().toArray(new LogicalType[0]);
- AggsHandlerCodeGenerator generator =
+
+ AggsHandlerCodeGenerator aggsGenerator =
new AggsHandlerCodeGenerator(
ctx,
relBuilder,
JavaScalaConversionUtil.toScala(Arrays.asList(fieldTypes)),
false); // copyInputField
- GeneratedAggsHandleFunction genAggsHandler =
- generator
+ aggsGenerator =
+ aggsGenerator
.needAccumulate()
// over agg code gen must pass the constants
- .withConstants(JavaScalaConversionUtil.toScala(constants))
- .generateAggsHandler("UnboundedOverAggregateHelper", aggInfoList);
+ .withConstants(JavaScalaConversionUtil.toScala(constants));
+
+ GeneratedAggsHandleFunction genAggsHandler =
+ aggsGenerator.generateAggsHandler("UnboundedOverAggregateHelper", aggInfoList);
LogicalType[] flattenAccTypes =
Arrays.stream(aggInfoList.getAccTypes())
.map(LogicalTypeDataTypeConverter::fromDataTypeToLogicalType)
.toArray(LogicalType[]::new);
- if (rowTimeIdx >= 0) {
- switch (unboundedOverVersion) {
- // Currently there is no migration path between first and second versions.
- case AbstractRowTimeUnboundedPrecedingOver.FIRST_OVER_VERSION:
- if (isRowsClause) {
- // ROWS unbounded over process function
- return new RowTimeRowsUnboundedPrecedingFunction<>(
+ switch (timeAttribute) {
+ case ROW_TIME:
+ final int rowTimeIdx = orderKeys[0];
+ switch (unboundedOverVersion) {
+ // Currently there is no migration path between first and second versions.
+ case AbstractRowTimeUnboundedPrecedingOver.FIRST_OVER_VERSION:
+ if (isRowsClause) {
+ // ROWS unbounded over process function
+ return new RowTimeRowsUnboundedPrecedingFunction<>(
+ config.getStateRetentionTime(),
+ TableConfigUtils.getMaxIdleStateRetentionTime(config),
+ genAggsHandler,
+ flattenAccTypes,
+ fieldTypes,
+ rowTimeIdx);
+ } else {
+ // RANGE unbounded over process function
+ return new RowTimeRangeUnboundedPrecedingFunction<>(
+ config.getStateRetentionTime(),
+ TableConfigUtils.getMaxIdleStateRetentionTime(config),
+ genAggsHandler,
+ flattenAccTypes,
+ fieldTypes,
+ rowTimeIdx);
+ }
+ case RowTimeUnboundedPrecedingOverFunctionV2.SECOND_OVER_VERSION:
+ return new RowTimeUnboundedPrecedingOverFunctionV2<>(
+ isRowsClause,
config.getStateRetentionTime(),
TableConfigUtils.getMaxIdleStateRetentionTime(config),
genAggsHandler,
flattenAccTypes,
fieldTypes,
rowTimeIdx);
- } else {
- // RANGE unbounded over process function
- return new RowTimeRangeUnboundedPrecedingFunction<>(
- config.getStateRetentionTime(),
- TableConfigUtils.getMaxIdleStateRetentionTime(config),
- genAggsHandler,
- flattenAccTypes,
- fieldTypes,
- rowTimeIdx);
- }
- case RowTimeUnboundedPrecedingOverFunctionV2.SECOND_OVER_VERSION:
- return new RowTimeUnboundedPrecedingOverFunctionV2<>(
- isRowsClause,
- config.getStateRetentionTime(),
- TableConfigUtils.getMaxIdleStateRetentionTime(config),
- genAggsHandler,
- flattenAccTypes,
- fieldTypes,
- rowTimeIdx);
- default:
- throw new UnsupportedOperationException(
- "Unsupported unbounded over version: "
- + unboundedOverVersion
- + ". Valid versions are 1 and 2.");
- }
- } else {
- return new ProcTimeUnboundedPrecedingFunction<>(
- StateConfigUtil.createTtlConfig(config.getStateRetentionTime()),
- genAggsHandler,
- flattenAccTypes);
+ default:
+ throw new UnsupportedOperationException(
+ "Unsupported unbounded over version: "
+ + unboundedOverVersion
+ + ". Valid versions are 1 and 2.");
+ }
+ case PROC_TIME:
+ return new ProcTimeUnboundedPrecedingFunction<>(
+ StateConfigUtil.createTtlConfig(config.getStateRetentionTime()),
+ genAggsHandler,
+ flattenAccTypes);
+ case NON_TIME:
+ if (isRowsClause) {
+ // Non-Time Rows Unbounded Preceding Function
+ throw new TableException(
+ "Non-Time Rows Unbounded Preceding Function not supported yet.");
Review Comment:
```suggestion
"OVER windows with UNBOUNDED PRECEDING are not supported when sorting on a non-time attribute column.");
```
##########
docs/content/docs/dev/table/sql/queries/over-agg.md:
##########
@@ -56,7 +56,7 @@ You can define multiple `OVER` window aggregates in a `SELECT` clause. However,
### ORDER BY
-`OVER` windows are defined on an ordered sequence of rows. Since tables do not have an inherent order, the `ORDER BY` clause is mandatory. For streaming queries, Flink currently only supports `OVER` windows that are defined with an ascending [time attributes]({{< ref "docs/dev/table/concepts/time_attributes" >}}) order. Additional orderings are not supported.
+`OVER` windows are defined on an ordered sequence of rows. Since tables do not have an inherent order, the `ORDER BY` clause is mandatory. For streaming queries, Flink currently supports `OVER` windows that are defined with an ascending [time attributes]({{< ref "docs/dev/table/concepts/time_attributes" >}}) order or ascending non-time attributes. Additional orderings are not supported.
Review Comment:
```suggestion
`OVER` windows are defined on an ordered sequence of rows. Since tables do not have an inherent order, the `ORDER BY` clause is mandatory. For streaming queries, Flink currently supports `OVER` windows that are defined with an ascending [time attribute]({{< ref "docs/dev/table/concepts/time_attributes" >}}) or ascending non-time attribute. Additional orderings are not supported.
```
##########
docs/content/docs/dev/table/sql/queries/over-agg.md:
##########
@@ -70,11 +70,20 @@ There are two options to define the range, `ROWS` intervals and `RANGE` interval
#### RANGE intervals
-A `RANGE` interval is defined on the values of the ORDER BY column, which is in case of Flink always a time attribute. The following RANGE interval defines that all rows with a time attribute of at most 30 minutes less than the current row are included in the aggregate.
+A `RANGE` interval is defined on the values of the ORDER BY column, which is in case of Flink is either a time or non-time attribute.
Review Comment:
```suggestion
A `RANGE` interval is defined on the values of the ORDER BY column, which (in case of Flink) is either a time or non-time attribute.
```
##########
flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecOverAggregate.java:
##########
@@ -439,33 +524,44 @@ private KeyedProcessFunction<RowData, RowData, RowData> createBoundedOverProcess
.map(LogicalTypeDataTypeConverter::fromDataTypeToLogicalType)
.toArray(LogicalType[]::new);
- if (rowTimeIdx >= 0) {
- if (isRowsClause) {
- return new RowTimeRowsBoundedPrecedingFunction<>(
- config.getStateRetentionTime(),
- TableConfigUtils.getMaxIdleStateRetentionTime(config),
- genAggsHandler,
- flattenAccTypes,
- fieldTypes,
- precedingOffset,
- rowTimeIdx);
- } else {
- return new RowTimeRangeBoundedPrecedingFunction<>(
- genAggsHandler, flattenAccTypes, fieldTypes, precedingOffset, rowTimeIdx);
- }
- } else {
- if (isRowsClause) {
- return new ProcTimeRowsBoundedPrecedingFunction<>(
- config.getStateRetentionTime(),
- TableConfigUtils.getMaxIdleStateRetentionTime(config),
- genAggsHandler,
- flattenAccTypes,
- fieldTypes,
- precedingOffset);
- } else {
- return new ProcTimeRangeBoundedPrecedingFunction<>(
- genAggsHandler, flattenAccTypes, fieldTypes, precedingOffset);
- }
+ switch (timeAttribute) {
+ case ROW_TIME:
+ final int rowTimeIdx = orderKeys[0];
+ if (isRowsClause) {
+ return new RowTimeRowsBoundedPrecedingFunction<>(
+ config.getStateRetentionTime(),
+ TableConfigUtils.getMaxIdleStateRetentionTime(config),
+ genAggsHandler,
+ flattenAccTypes,
+ fieldTypes,
+ precedingOffset,
+ rowTimeIdx);
+ } else {
+ return new RowTimeRangeBoundedPrecedingFunction<>(
+ genAggsHandler,
+ flattenAccTypes,
+ fieldTypes,
+ precedingOffset,
+ rowTimeIdx);
+ }
+ case PROC_TIME:
+ if (isRowsClause) {
+ return new ProcTimeRowsBoundedPrecedingFunction<>(
+ config.getStateRetentionTime(),
+ TableConfigUtils.getMaxIdleStateRetentionTime(config),
+ genAggsHandler,
+ flattenAccTypes,
+ fieldTypes,
+ precedingOffset);
+ } else {
+ return new ProcTimeRangeBoundedPrecedingFunction<>(
+ genAggsHandler, flattenAccTypes, fieldTypes, precedingOffset);
+ }
+ case NON_TIME:
+ throw new TableException(
+ "Non-time attribute sort is not supported for bounded over aggregate");
+ default:
+ throw new TableException("Unsupported bounded operation");
Review Comment:
```suggestion
throw new TableException("Unsupported bounded operation for OVER window.");
```
##########
flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/OverAggregateNDUTest.java:
##########
@@ -0,0 +1,129 @@
+/*
+ * 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.flink.table.planner.plan.nodes.exec.stream;
+
+import org.apache.flink.table.api.TableConfig;
+import org.apache.flink.table.api.TableEnvironment;
+import org.apache.flink.table.api.TableException;
+import org.apache.flink.table.api.config.OptimizerConfigOptions;
+import org.apache.flink.table.api.config.OptimizerConfigOptions.NonDeterministicUpdateStrategy;
+import org.apache.flink.table.planner.utils.StreamTableTestUtil;
+import org.apache.flink.table.planner.utils.TableTestBase;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/** Test NDU errors for over aggregate. */
+class OverAggregateNDUTest extends TableTestBase {
Review Comment:
can we move this into `NonDeterministicUpdateAnalyzerTest`? It might fit better than the exec/stream package.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,688 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.StateTtlConfig;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+import static org.apache.flink.table.runtime.util.StateConfigUtil.createTtlConfig;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
Review Comment:
I saw that the methods have good JavaDoc. But could you add a bit more JavaDoc for a very high level summary how this operator works. what are the base assumptions? what are the IDs stored in state? what was you goal? limitations?
##########
flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalOverAggregate.scala:
##########
@@ -27,9 +29,12 @@ import org.apache.calcite.plan.{RelOptCluster, RelTraitSet}
import org.apache.calcite.rel.`type`.RelDataType
import org.apache.calcite.rel.RelNode
import org.apache.calcite.rel.core.Window
+import org.apache.calcite.util.ImmutableBitSet
Review Comment:
remove unnecessary changes
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on PR #25753:
URL: https://github.com/apache/flink/pull/25753#issuecomment-2540303200
@lincoln-lil I noticed that you made some similar changes (more specifically the planner changes) for optimizing the Deduplicate operator. Would you mind giving me some suggestions regarding some planner specific changes that needs to be done if we want to support retract mode for a specific operator? I looked at your PR and I think I would need to also update the NDU files. Any other files/classes that I should look into?
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "fhueske (via GitHub)" <gi...@apache.org>.
fhueske commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1907085683
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
Review Comment:
This code can also be put into a helper method `insertNewRow(input)` which is reused above.
The helper method should IMO also include the next lines:
```
sortedKeyState.update(sortedKeyList);
idState.update(id);
```
so that all state updates related to adding the new row are in a single helper method
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
Review Comment:
```suggestion
long curKey = iterator.next().getLong(keyIdx);
```
?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
Review Comment:
move these lines into a helper method that handles inserting a new row into the various state objects.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
Review Comment:
```suggestion
// If previous accumulator is same as the new accumulator
```
##########
flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunctionTest.java:
##########
@@ -0,0 +1,467 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.streaming.api.operators.KeyedProcessOperator;
+import org.apache.flink.streaming.api.watermark.Watermark;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordComparator;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.types.RowKind;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.insertRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateAfterRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateBeforeRecord;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for {@link NonTimeUnboundedPrecedingFunction}. */
+public class NonTimeUnboundedPrecedingFunctionTest extends RowTimeOverWindowTestBase {
+
+ private static final int SORT_KEY_IDX = 1;
+
+ private static final FieldGetter SORT_KEY_FIELD_GETTER =
+ RowData.createFieldGetter(DataTypes.BIGINT().getLogicalType(), SORT_KEY_IDX);
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(SORT_KEY_IDX);
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_RECORD_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ // return new RowDataRecordEqualiser();
+ return new TestRecordEqualiser();
+ }
+ };
+
+ /** Custom test comparator for comparing numbers. */
+ public static class LongRecordComparator implements RecordComparator {
+
+ private int pos;
+
+ public LongRecordComparator(int pos) {
+ this.pos = pos;
+ }
+
+ @Override
+ public int compare(RowData o1, RowData o2) {
+ boolean null0At1 = o1.isNullAt(pos);
+ boolean null0At2 = o2.isNullAt(pos);
+ int cmp0 =
+ null0At1 && null0At2
+ ? 0
+ : (null0At1
+ ? -1
+ : (null0At2
+ ? 1
+ : Long.compare(o1.getLong(pos), o2.getLong(pos))));
+ if (cmp0 != 0) {
+ return cmp0;
+ }
+ return 0;
+ }
+ }
+
+ /** Custom test row equaliser for comparing rows. */
+ public static class TestRecordEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getString(0).equals(row2.getString(0))
+ && row1.getLong(1) == row2.getLong(1)
+ && row1.getLong(2) == row2.getLong(2);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ @Test
+ public void testInsertOnlyRecordsWithCustomSortKey() throws Exception {
+ NonTimeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeUnboundedPrecedingFunction<RowData>(
+ 0,
+ 2000,
+ aggsHandleFunction,
Review Comment:
`aggsHandleFunction` uses a `SUM` function. We should either change this to `AVG` or add a new `avgAggsHandleFunction` for our tests.
In contrast to the `SUM` function, the `AVG` function uses different types for the accumulator and result value.
Using `AVG` should have caught all issues in which accumulators and values were used interchangeably.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+
+ currFunction.accumulate(curValue);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ RowData curAcc = accMapState.get(curKey.getLong(keyIdx));
+ currFunction.setAccumulators(curAcc);
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+
+ valueMapState.remove(curKey.getLong(keyIdx));
+ accMapState.remove(curKey.getLong(keyIdx));
+ if (iterator.hasPrevious()) {
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ RowData prevAcc = accMapState.get(prev.getLong(keyIdx));
+ currFunction.setAccumulators(prevAcc);
Review Comment:
could reuse the helper method suggested above: `setAccOfPreviousRow(iterator)`
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
Review Comment:
couldn't we insert `inputKey` here instead of creating a new object?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
Review Comment:
```suggestion
accMapState.put(id, currFunction.getAccumulators());
```
I think we need to store the accumulators here and not the value. For some functions, these are the same (count, sum, max, min) but for some they are not (avg, ...).
It would be good to have some tests with agg functions for which the accumulator is different from the value.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
Review Comment:
I just realized that the order by logic is more involved than we thought.
If multiple rows have the same sort key, they all have the same aggregated value.
See this simple example from MySQL as an example:
```
mysql> select num, val, sum(val) over (order by num) oversum from tt;
+------+------+---------+
| num | val | oversum |
+------+------+---------+
| 1 | 1 | 6 |
| 1 | 2 | 6 |
| 1 | 3 | 6 |
| 2 | 3 | 9 |
| 3 | 4 | 13 |
+------+------+---------+
```
We could address this by storing not a single id per sort key, but a list of ids for each sort key.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
Review Comment:
We might also want to compare the *values* that were computed from the previous and new accumulators.
If these are the same, we don't need to update the current row, because it did not change.
However, we should not `break` in this case but just `continue`.
The reasoning is that the same accumulator will always result in the same computed value. However, different accumulators might result in the same computed value.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
Review Comment:
```suggestion
Long prevKey = sortedKeyList.get(iterator.previousIndex()).getLong(keyIdx);
RowData prevAcc = accMapState.get(prevKey);
currFunction.setAccumulators(prevAcc);
```
put this into a helper method `setAccOfPreviousRow(iterator)` and reuse it above (and in the remove code path)?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
Review Comment:
Why don't set the `id` already here and insert this later into the list?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
Review Comment:
```suggestion
accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+
+ currFunction.accumulate(curValue);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
Review Comment:
Before we compare the full row, we can compare just on the sort key which is already deserialized.
This means, we don't need to deserialize any rows before we reach rows with the same sort key
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
Review Comment:
we are comparing here the previous *accumulator* with the new *value* which is not correct.
we need to compare the previous accumulator with the current accumulator, i.e.,
```
prevAcc.equals(currFunction.getAccumulators())
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+
+ currFunction.accumulate(curValue);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ RowData curAcc = accMapState.get(curKey.getLong(keyIdx));
+ currFunction.setAccumulators(curAcc);
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+
+ valueMapState.remove(curKey.getLong(keyIdx));
+ accMapState.remove(curKey.getLong(keyIdx));
+ if (iterator.hasPrevious()) {
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ RowData prevAcc = accMapState.get(prev.getLong(keyIdx));
+ currFunction.setAccumulators(prevAcc);
+ } else {
+ currFunction.setAccumulators(currFunction.createAccumulators());
+ }
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
Review Comment:
this could be moved up to the statements that update the `valueMapState` and `accMapState`.
That would keep all related state updates together.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
Review Comment:
We need to compute the previous *value* from the previous *accumulator*. They might not be the same (and also have different data types, e.g., for `AVG`). In your tests this is probably working because for some functions `accumulator == value` (count, sum, min, max).
For that we either need a second `function` or use `currFunction` and rotate its accumulators.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
Review Comment:
The logic in this loop can be reused in the `remove` method (after the retracted row was removed from the state).
We should move the code into a helper method `updateSubsequentRows(iterator)`.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
Review Comment:
In fact, I'm not sure if we handle this case correctly for the time-ordered over aggregates.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
Review Comment:
```suggestion
RowData curKey = iterator.next().getLong(keyIdx);
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
Review Comment:
We should use the helper method `updateSubsequentRows(iterator)` suggested above and reuse the logic.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
Review Comment:
IMO, the code would be a bit easier to follow if the method was similarly structured as the `insert` method, i.e.,
* use the iterator to find the row to retract, remove the row from the list and map states and retract the row
* update the following accumulators and emit the update records
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
Review Comment:
We should compare the previous accumulator with the new accumulator (not the new value).
For the new value, we also need to `accumulate()` the current value first.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1908156348
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
Review Comment:
For time-ordered over aggregates, ORDER BY is only supported for 1 time-column and only in ASC order.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1908077945
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
Review Comment:
Fixed
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
Review Comment:
Fixed
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928128562
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
Review Comment:
Changed helper methods to use collectUpdateBefore, collectUpdateAfter, etc.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
Review Comment:
Also added JavaDocs
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928134386
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
Review Comment:
In the new implementation the code for remove is similarly structured as the code for insert
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928127583
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
+ prevAcc = currFunction.getAccumulators();
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ prevAcc = accMapState.get(ids.get(i));
+ currFunction.accumulate(curValue);
+ accMapState.put(ids.get(i), currFunction.getAccumulators());
+ i++;
+ }
+ i = 0;
+ } else {
+ prevAcc = accMapState.get(ids.get(ids.size() - 2));
+ }
+
+ while (i < ids.size()) {
+ RowData curValue = valueMapState.get(ids.get(i));
+ if (ids.get(i) == insertedId) {
+ sendUpdate(curValue, RowKind.INSERT, out);
+ } else {
+ sendUpdateBefore(curValue, prevAcc, out);
+ sendUpdateAfter(curValue, out);
+ if (i != 0 && insertedId == -1) {
+ iterator.next();
+ }
+ }
+ i++;
+ }
+ }
+
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ // Update single row
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ } else if (equaliser.equals(curValue, input)) {
+ List<Long> ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ removeRowFromSameSortKeys(sortedKeyList, curKey, iterator, ids, out);
+ } else {
+ // Retract single record
+ RowData curAcc = accMapState.get(curKey.getLong(keyIdx));
+ sendUpdateBefore(input, curAcc, out);
+ iterator.remove();
+ removeFromState(sortedKeyList, ids, curKey.getLong(keyIdx), -1, curKey.getLong(1));
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ } else {
+ currFunction.setAccumulators(currFunction.createAccumulators());
+ }
+ }
+ isRetracted = true;
+ }
+ }
+ }
+
+ private void removeRowFromSameSortKeys(List<RowData> sortedKeyList, RowData curKey, ListIterator<RowData> iterator, List<Long> ids, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ int removeIndex = -1;
+ while (i < ids.size()) {
Review Comment:
New implementation uses for-loops.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928130188
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
+ sendUpdateAfter(curValue, out);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getAccumulators());
+
+ return true;
+ }
+
+ private void updateSameSortKeys(ListIterator<RowData> iterator, List<Long> ids, Long insertedId, Collector<RowData> out)
+ throws Exception {
+ int i = 0;
+ RowData prevAcc;
+ if (insertedId == -1) {
Review Comment:
Restructured and refactored the code
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928129417
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ // break;
+ return false;
+ }
+
+ sendUpdateBefore(curValue, prevAcc, out);
+ currFunction.accumulate(curValue);
Review Comment:
See new implementation
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "fhueske (via GitHub)" <gi...@apache.org>.
fhueske commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1930153752
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,578 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
Review Comment:
Update comment to List of IDs (instead of single id per list entry)
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,578 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList.
Review Comment:
```suggestion
* Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
* List<Long Ids>>. Extracts the inputSortKey from the input and compares it with every element
* in the sortedList. If an sortKey already exists in the sortedList for the input, add the id to
* the list of ids and update the sortedList. Otherwise find the right position in the sortedList
* and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
* end of the sortedList.
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
Review Comment:
```suggestion
// register state-cleanup timer for the current key
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
Review Comment:
```suggestion
currFunction.getAccumulators(),
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
Review Comment:
```suggestion
protected transient AggsHandleFunction aggFuncs;
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
Review Comment:
```suggestion
* Puts an element from the input stream into state or removes it from state if the input is a retraction.
Emits the aggregated value for the newly inserted element and updates all results that are affected by the added or removed row. Emits the same aggregated value for all elements with the same sortKey to comply
* with the sql RANGE syntax.
```
not sure if the distinction between append stream and retract stream is useful. A retract stream might also have insert-rows.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
Review Comment:
```suggestion
Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
```
we should pass the agg value not the accumulator into this method.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
Review Comment:
```suggestion
* Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
* List<Long Ids>>. Extracts the inputSortKey from the input and compares it with every element
```
nit: The code formatter automatically breaks and merges lines of JavaDocs. Please make sure to end sentences with periods for better readability.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
Review Comment:
```suggestion
collectInsertOrUpdateAfter(out, input, currFunction.getValue(), origRowKind);
```
explicitly pass the aggregated value instead of relying on the right state of `currFunction()`
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
Review Comment:
```suggestion
id = Long.MIN_VALUE;
```
Start at MIN_VALUE to double the available value range?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
Review Comment:
Shouldn't this be `collectDelete()`?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
Review Comment:
This is essentially the same code as for the "Insert in the middle" case above. Could this be shared code?
For example, if we change logic in a way that we first call a method that finds the position at which to insert the new row. The method would return an index and a boolean if the value needs to be inserted at (bc it has the same sort key) or after that index.
Then we only would need to handle the cases for inserting in an existing list or inserting as single row.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
Review Comment:
This check should be moved out of the (inner) `for` loop because the accumulators are the same for all rows with the same sortKey.
Also, we want to break out of the outer `for` loop. If the accumulators are identical, all following accumulators will be identical too since the data is the same as before.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
Review Comment:
```suggestion
// accumulator equality check goes in front of inner for loop
if (accumulators are equal) {
break;
}
// compute agg values
RowData currAggVal = currFunction.getValue();
currFunction.setAccumulator(prevAcc);
RowData prevAggVal = currFunction.getValue();
for (int j = 0; j < ids.size(); j++) {
```
We need to compute the aggregation value from the accumulators.
Unfortunately, we need to load the acc into the function to do that.
A helper function for this would be great.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
Review Comment:
```suggestion
int idxOfChangedRow,
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching input from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
Review Comment:
```suggestion
private void removeFromSortedList(RowData delRow, Collector<RowData> out) throws Exception {
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
Review Comment:
You could extract the logic to find the right position in the `sortedList` into a helper method.
Then it would be easier to improve it, e.g., with binary search, or checking at the end of the list first (might be a common pattern?)
But that's just an optional suggestion.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
Review Comment:
"If no matching sortKey is found, add a new entry in the end of the sortedList"
This is not really true. A new entry might also be inserted somewhere in the list (not necessarily at the end).
The row is either added to an existing sortKey entry or we create a new entry at the right position within the list.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
Review Comment:
```suggestion
for (int j = 0; j < ids.size() - 1; j++) {
// Get rowValue for id and re-accumulate it
RowData value = valueMapState.get(ids.get(j));
currFunction.accumulate(value);
}
currFunction.accumulate(input);
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching input from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ int i;
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, input);
+ if (removeIndex == -1) {
+ // TODO: can this ever happen? may be during/after state cleaning?
+ LOG.info("Could not find id to remove");
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey),
+ currFunction.getAccumulators(),
+ RowKind.DELETE,
+ input,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(sortedList, deletedId, curSortKey);
+ break;
+ }
+ }
Review Comment:
we should check if we found the row. If not, we should handle the case the same way as above (log a message and return).
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
Review Comment:
Is `RowData.equals()` a proper equality check? Don't we need a code-generated comparator for that?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching input from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ int i;
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, input);
+ if (removeIndex == -1) {
+ // TODO: can this ever happen? may be during/after state cleaning?
+ LOG.info("Could not find id to remove");
Review Comment:
The log line should give a few more details. Also this is not about an `id` but a row that could not be retracted.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
Review Comment:
Before entering the loop, we can compare the accumulators. If they are the same, we only need to emit the change for the input row but not for any of the rows with the same sort key.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
Review Comment:
```suggestion
RowData changedRow,
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
Review Comment:
Should this ever happen? If the state was cleaned due to TTL, also the list state would have been empty and `prevIndex` should have been `null`, right?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching input from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ int i;
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, input);
+ if (removeIndex == -1) {
+ // TODO: can this ever happen? may be during/after state cleaning?
+ LOG.info("Could not find id to remove");
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey),
+ currFunction.getAccumulators(),
+ RowKind.DELETE,
+ input,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(sortedList, deletedId, curSortKey);
+ break;
+ }
+ }
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids without the id that will be
+ * removed.
+ *
+ * @param ids
+ * @param input
+ * @return the index position of the id that should be removed
+ * @throws Exception
+ */
+ private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData input) throws Exception {
+ int removeIndex = -1;
+ for (int j = 0; j < ids.size(); j++) {
+ RowData curValue = valueMapState.get(ids.get(j));
+ if (valueEqualiser.equals(curValue, input)) {
+ removeIndex = j;
+ } else {
+ currFunction.accumulate(curValue);
+ }
+ }
+ return removeIndex;
+ }
+
+ /**
+ * Remove the ith element from the sortedList if the list of ids is empty and return the ith
+ * location OR update the sorted list with the list of ids and return the next location of the
+ * sortedList.
+ *
+ * @param sortedList
+ * @param idx
+ * @param ids
+ * @param curSortKey
+ * @return
+ */
+ private int removeIdFromSortedList(
+ List<Tuple2<RowData, List<Long>>> sortedList,
+ int idx,
+ List<Long> ids,
+ RowData curSortKey) {
+ if (ids.isEmpty()) {
+ sortedList.remove(idx);
+ } else {
+ // Update sortedList without the removed id
+ sortedList.set(idx, new Tuple2<RowData, List<Long>>(curSortKey, ids));
+ idx++;
+ }
+ return idx;
+ }
+
+ private void removeFromState(
+ List<Tuple2<RowData, List<Long>>> sortedList, Long id, RowData sortKey)
+ throws Exception {
+ valueMapState.remove(id);
+ accMapState.put(sortKey, currFunction.getAccumulators());
Review Comment:
if this was the last row with the same sort key, we have to delete the accumulator from the map.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
Review Comment:
```suggestion
Collector<RowData> out, RowData rowValue, RowData currAggValue) {
```
we should pass the agg value not the accumulator into this method.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
Review Comment:
```suggestion
private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAggValue) {
```
we should pass the agg value not the accumulator into this method.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
Review Comment:
we should not pass the accumulator into this method but the aggregated value.
for that, we need to put the acc into the function (`currFunction` or `aggFuncs`) and call `getValue()`.
Since the accumulator is the same for all rows in the same sortKey group, we can do. that outside of the loop.
This needs to be done for `prevAcc` and `currAcc`.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
Review Comment:
```suggestion
LOG.debug("Prev accumulator is same as curr accumulator. Skipping updates.");
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
Review Comment:
I think it's a bit risky to rely on the correct state of the current function.
I would rather pass in the `currAggValue` directly, as done for the other `collect` methods.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
Review Comment:
great that this is abstracted!
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
Review Comment:
Is this a proper value-equality check? Don't we need a code-generated comparator for this?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
Review Comment:
pass in the accumulator before the sortedList index and explicitly put it into `currFunction`?
I think it's a bit dangerous to rely on the implicit state of `currFunction`.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
Review Comment:
```suggestion
LOG.debug("Prev accumulator is same as curr accumulator. Skipping updates.");
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
Review Comment:
```suggestion
private void insertIntoSortedList(RowData insRow, Collector<RowData> out) throws Exception {
```
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching input from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ int i;
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, input);
+ if (removeIndex == -1) {
+ // TODO: can this ever happen? may be during/after state cleaning?
Review Comment:
This can happen in a few ways. State TTL having cleaned the state before might be one reason. Others could be changelog input that wasn't correctly bootstrapped.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
Review Comment:
These mixed-up usages of accumulator and value don't seem to be caught by unit tests.
IMO, that's a bit concerning.
It would be good to have tests in place that ensure that we are storing the right things in state and emitting the right things.
Using the AVG function in tests instead of SUM or COUNT should do the trick.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,596 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList. After and element is inserted, an INSERT/UPDATE_AFTER event is emitted
+ * for the newly inserted element, and for all subsequent elements an UPDATE_BEFORE and
+ * UPDATE_AFTER event is emitted based on the previous and newly aggregated values. Updates are
+ * skipped if the previously accumulated value is the same as the newly accumulated value to
+ * save on network bandwidth.
+ *
+ * @param input The input value.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ Long id = getNextId();
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ int i;
+ boolean isInserted = false;
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyComparator.compare(curSortKey, inputSortKey) == 0) {
+ // Insert at the (i)th location by adding the new id to the list of ids
+ ids.add(id);
+ sortedList.set(i, new Tuple2<>(curSortKey, ids));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ reAccumulateIdsAfterInsert(ids, input);
+ emitUpdatesForIds(
+ ids,
+ ids.size() - 1,
+ accMapState.get(curSortKey),
+ currFunction.getValue(),
+ origRowKind,
+ input,
+ out);
+ isInserted = true;
+ i++;
+ break;
+ } else if (sortKeyComparator.compare(curSortKey, inputSortKey) > 0) {
+ // Insert in the middle
+ sortedList.add(i, new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ isInserted = true;
+ i++;
+ break;
+ }
+ }
+
+ if (!isInserted) {
+ // Insert at the end
+ sortedList.add(new Tuple2<>(inputSortKey, List.of(id)));
+ setAccumulatorOfPrevRow(sortedList, sortedList.size() - 2);
+ currFunction.accumulate(input);
+ collectInsertOrUpdateAfter(out, input, origRowKind);
+ i++;
+ }
+
+ addToState(id, input, inputSortKey, currFunction.getAccumulators(), sortedList);
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * @return the next id after reading from the idState
+ * @throws IOException
+ */
+ private Long getNextId() throws IOException {
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ return id;
+ }
+
+ /**
+ * @return the sortedList containing sortKeys and Ids with the same sortKey
+ * @throws IOException
+ */
+ private List<Tuple2<RowData, List<Long>>> getSortedList() throws IOException {
+ List<Tuple2<RowData, List<Long>>> sortedList = sortedListState.value();
+ if (sortedList == null) {
+ sortedList = new ArrayList<>();
+ }
+ return sortedList;
+ }
+
+ /**
+ * Helper method to set the accumulator based on the prevIndex
+ *
+ * @param sortedList
+ * @param prevIndex
+ * @throws Exception
+ */
+ private void setAccumulatorOfPrevRow(
+ List<Tuple2<RowData, List<Long>>> sortedList, int prevIndex) throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ RowData prevAcc = accMapState.get(sortedList.get(prevIndex).f0);
+ if (prevAcc == null) {
+ // TODO: Throw exception since this could due to state cleaning
+ currFunction.createAccumulators();
+ } else {
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids after an id was inserted to
+ * the end of the ids list.
+ *
+ * @param ids
+ * @param input
+ * @throws Exception
+ */
+ private void reAccumulateIdsAfterInsert(List<Long> ids, RowData input) throws Exception {
+ // Update acc value for all ids
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == ids.size() - 1) {
+ // Update acc for newly inserted value
+ currFunction.accumulate(input);
+ } else {
+ // Get rowValue for id and re-accumulate it
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ }
+ }
+
+ /**
+ * Emits updates for all ids. The id belonging to idx location either emits a DELETE or
+ * INSERT/UPDATE_AFTER, depending on whether the id is being added to ids or removed from ids.
+ * All other ids except the id at position idx emit old and new aggregated values in the form of
+ * UPDATE_BEFORE and UPDATE_AFTER respectively.
+ *
+ * @param ids
+ * @param idx
+ * @param prevAcc
+ * @param currAcc
+ * @param rowKind
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void emitUpdatesForIds(
+ List<Long> ids,
+ int idx,
+ RowData prevAcc,
+ RowData currAcc,
+ RowKind rowKind,
+ RowData input,
+ Collector<RowData> out)
+ throws Exception {
+ for (int j = 0; j < ids.size(); j++) {
+ if (j == idx) {
+ if (rowKind == RowKind.DELETE) {
+ // TODO: change to collectDelete
+ collectUpdateBefore(out, input, prevAcc);
+ } else {
+ // For insert, emit either Insert or UpdateAfter
+ collectInsertOrUpdateAfter(out, input, rowKind);
+ }
+ } else {
+ if (prevAcc.equals(currAcc)) {
+ // Previous accumulator is the same as the current accumulator
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ continue;
+ }
+ RowData value = valueMapState.get(ids.get(j));
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currAcc);
+ }
+ }
+ }
+
+ private void addToState(
+ Long id,
+ RowData input,
+ RowData inputSortKey,
+ RowData accumulators,
+ List<Tuple2<RowData, List<Long>>> sortedList)
+ throws Exception {
+ valueMapState.put(id, input);
+ accMapState.put(inputSortKey, accumulators);
+ sortedListState.update(sortedList);
+ idState.update(++id);
+ }
+
+ /**
+ * Process all sort keys starting from startPos location. Calculates the new agg value for all
+ * ids belonging to the same sort key. Finally, emits the old and new aggregated values for the
+ * ids if the newly aggregated value is different from the previously aggregated values.
+ *
+ * @param sortedList
+ * @param startPos
+ * @param out
+ * @throws Exception
+ */
+ private void processRemainingElements(
+ List<Tuple2<RowData, List<Long>>> sortedList, int startPos, Collector<RowData> out)
+ throws Exception {
+ // Send updates for remaining sort keys after the inserted/removed idx
+ for (int i = startPos; i < sortedList.size(); i++) {
+ Tuple2<RowData, List<Long>> sortKeyAndIds = sortedList.get(i);
+ RowData curSortKey = sortKeyAndIds.f0;
+ List<Long> ids = sortKeyAndIds.f1;
+ // Get previous accumulator
+ RowData prevAcc = accMapState.get(curSortKey);
+
+ // Calculate new agg value for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ currFunction.accumulate(value);
+ }
+ // Emit old and new agg values for all ids with the same sortKey
+ for (int j = 0; j < ids.size(); j++) {
+ RowData value = valueMapState.get(ids.get(j));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
+ // Previous accumulator is the same as the current accumulator.
+ // This means all the ids will have no change in the accumulated value.
+ // Skip sending downstream updates in such cases to reduce network traffic
+ LOG.info("Prev accumulator is same as curr accumulator. Skipping updates.");
+ break;
+ }
+ collectUpdateBefore(out, value, prevAcc);
+ collectUpdateAfter(out, value, currFunction.getValue());
+ }
+ // update accumulated state for sortKey if prevAcc != currAcc
+ if (!prevAcc.equals(currFunction.getAccumulators())) {
+ accMapState.put(curSortKey, currFunction.getAccumulators());
+ }
+ }
+ }
+
+ private void collectInsertOrUpdateAfter(Collector<RowData> out, RowData value, RowKind rowKind)
+ throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void collectUpdateBefore(
+ Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ private void collectUpdateAfter(
+ Collector<RowData> out, RowData rowValue, RowData currAccValue) {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(rowValue, currAccValue);
+ out.collect(output);
+ }
+
+ private void collectDelete(Collector<RowData> out, RowData rowValue, RowData prevAccValue) {
+ output.setRowKind(RowKind.DELETE);
+ output.replace(rowValue, prevAccValue);
+ out.collect(output);
+ }
+
+ /**
+ * Removes the row matching input from the sortedList. Matching is done based on the sortKeys.
+ * Once matched, the actual row is compared for all ids belonging to the same sortKey. After the
+ * element is removed, a DELETE event is emitted for the removed element, and for all subsequent
+ * elements an UPDATE_BEFORE and UPDATE_AFTER event is emitted based on the previous and newly
+ * aggregated values. Updates are skipped if the previously accumulated value is the same as the
+ * newly accumulated value to save on network bandwidth.
+ *
+ * @param input
+ * @param out
+ * @throws Exception
+ */
+ private void removeFromSortedList(RowData input, Collector<RowData> out) throws Exception {
+ int i;
+ input.setRowKind(RowKind.INSERT);
+ RowData inputSortKey = sortKeySelector.getKey(input);
+ List<Tuple2<RowData, List<Long>>> sortedList = getSortedList();
+
+ for (i = 0; i < sortedList.size(); i++) {
+ RowData curSortKey = sortedList.get(i).f0;
+ List<Long> ids = new ArrayList<>(sortedList.get(i).f1);
+ if (sortKeyEqualiser.equals(curSortKey, inputSortKey)) {
+ setAccumulatorOfPrevRow(sortedList, i - 1);
+ // Accumulate all ids except id which needs to be removed
+ int removeIndex = reAccumulateIdsAndGetRemoveIndex(ids, input);
+ if (removeIndex == -1) {
+ // TODO: can this ever happen? may be during/after state cleaning?
+ LOG.info("Could not find id to remove");
+ return;
+ }
+ emitUpdatesForIds(
+ ids,
+ removeIndex,
+ accMapState.get(curSortKey),
+ currFunction.getAccumulators(),
+ RowKind.DELETE,
+ input,
+ out);
+ Long deletedId = ids.remove(removeIndex);
+ i = removeIdFromSortedList(sortedList, i, ids, curSortKey);
+ removeFromState(sortedList, deletedId, curSortKey);
+ break;
+ }
+ }
+
+ processRemainingElements(sortedList, i, out);
+ }
+
+ /**
+ * Helper method to re-accumulate the aggregated value for all ids without the id that will be
+ * removed.
+ *
+ * @param ids
+ * @param input
+ * @return the index position of the id that should be removed
+ * @throws Exception
+ */
+ private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData input) throws Exception {
Review Comment:
```suggestion
private int reAccumulateIdsAndGetRemoveIndex(List<Long> ids, RowData delRow) throws Exception {
```
easier to understand that the row is about to be deleted / removed.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1957702817
##########
flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunctionTest.java:
##########
@@ -0,0 +1,2028 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.streaming.api.operators.KeyedProcessOperator;
+import org.apache.flink.streaming.api.watermark.Watermark;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordComparator;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.BigIntType;
+import org.apache.flink.table.types.logical.IntType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.VarCharType;
+import org.apache.flink.table.utils.HandwrittenSelectorUtil;
+import org.apache.flink.types.RowKind;
+
+import org.junit.Test;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.insertRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateAfterRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateBeforeRecord;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for {@link NonTimeRangeUnboundedPrecedingFunction}. */
+public class NonTimeRangeUnboundedPrecedingFunctionTest extends RowTimeOverWindowTestBase {
+
+ private static final int SORT_KEY_IDX = 1;
+
+ private static final LogicalType[] SORT_KEY_TYPES = new LogicalType[] {new BigIntType()};
+
+ private static final InternalTypeInfo<RowData> INPUT_ROW_TYPE =
+ InternalTypeInfo.ofFields(VarCharType.STRING_TYPE, new BigIntType(), new IntType());
+
+ private static final RowDataKeySelector SORT_KEY_SELECTOR =
+ HandwrittenSelectorUtil.getRowDataSelector(
+ new int[] {SORT_KEY_IDX}, INPUT_ROW_TYPE.toRowFieldTypes());
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR_ASC =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(0, true);
+ }
+ };
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR_DESC =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(0, false);
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_ROW_VALUE_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ return new TestRowValueEqualiser();
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_SORT_KEY_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ return new TestSortKeyEqualiser();
+ }
+ };
+
+ /** Custom test comparator for comparing numbers. */
+ public static class LongRecordComparator implements RecordComparator {
+
+ private int pos;
+ private boolean isAscending;
+
+ public LongRecordComparator(int pos, boolean isAscending) {
+ this.pos = pos;
+ this.isAscending = isAscending;
+ }
+
+ @Override
+ public int compare(RowData o1, RowData o2) {
+ boolean null0At1 = o1.isNullAt(pos);
+ boolean null0At2 = o2.isNullAt(pos);
+ int cmp0 =
+ null0At1 && null0At2
+ ? 0
+ : (null0At1
+ ? -1
+ : (null0At2
+ ? 1
+ : Long.compare(o1.getLong(pos), o2.getLong(pos))));
+ if (cmp0 != 0) {
+ if (isAscending) {
+ return cmp0;
+ } else {
+ return -cmp0;
+ }
+ }
+ return 0;
+ }
+ }
+
+ /** Custom test row equaliser for comparing rows. */
+ public static class TestRowValueEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getString(0).equals(row2.getString(0))
+ && row1.getLong(1) == row2.getLong(1)
+ && row1.getLong(2) == row2.getLong(2);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ /** Custom test sortKey equaliser for comparing sort keys. */
+ public static class TestSortKeyEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getLong(0) == row2.getLong(0);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ @Test
+ public void testInsertOnlyRecordsWithCustomSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(insertRecord("key2", 1L, 100L));
+ testHarness.processElement(insertRecord("key2", 2L, 200L));
+
+ testHarness.processWatermark(new Watermark(500L));
+
+ // out of order record should trigger updates for all records after its inserted position
+ testHarness.processElement(insertRecord("key1", 4L, 400L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 14L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 7L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 12L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 14L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testInsertOnlyRecordsWithDuplicateSortKeys() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 5L, 502L));
+ testHarness.processElement(insertRecord("key1", 5L, 501L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(insertRecord("key2", 1L, 100L));
+ testHarness.processElement(insertRecord("key2", 2L, 200L));
+
+ testHarness.processWatermark(new Watermark(500L));
+
+ // out of order record should trigger updates for all records after its inserted position
+ testHarness.processElement(insertRecord("key1", 2L, 203L));
+ testHarness.processElement(insertRecord("key1", 2L, 201L));
+ testHarness.processElement(insertRecord("key1", 4L, 400L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 24L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 203L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 18L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 24L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 7L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 203L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 203L),
+ GenericRowData.ofKind(RowKind.INSERT, 7L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 7L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 28L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 11L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 22L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 28L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 32L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testRetractingRecordsWithCustomSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(updateBeforeRecord("key1", 2L, 200L));
+ testHarness.processElement(updateAfterRecord("key1", 3L, 200L));
+ testHarness.processElement(insertRecord("key2", 1L, 100L));
+ testHarness.processElement(insertRecord("key2", 2L, 200L));
+ testHarness.processElement(insertRecord("key3", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 4L, 400L));
+ testHarness.processElement(updateBeforeRecord("key1", 3L, 200L));
+ testHarness.processElement(updateAfterRecord("key1", 3L, 300L));
+
+ // Watermark has no impact and should be ignored
+ testHarness.processWatermark(new Watermark(500L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 14L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 6L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 14L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 12L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 3L,
+ 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 4L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 6L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 9L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 12L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key2"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key3"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 9L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 19L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 3L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 4L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 19L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 16L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 3L,
+ 300L),
+ GenericRowData.ofKind(RowKind.INSERT, 4L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 4L, 400L),
+ GenericRowData.ofKind(RowKind.INSERT, 8L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 16L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 19L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testRetractWithFirstDuplicateSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 2L, 201L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 5L, 502L));
+ testHarness.processElement(insertRecord("key1", 5L, 501L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(updateBeforeRecord("key1", 5L, 500L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 21L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testRetractWithMiddleDuplicateSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 2L, 201L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 5L, 502L));
+ testHarness.processElement(insertRecord("key1", 5L, 501L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(updateBeforeRecord("key1", 5L, 502L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 21L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testRetractWithLastDuplicateSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 2L, 201L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 5L, 502L));
+ testHarness.processElement(insertRecord("key1", 5L, 501L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(updateBeforeRecord("key1", 5L, 501L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 6L,
+ 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 21L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testRetractWithDescendingSort() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_DESC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 2L, 201L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(updateBeforeRecord("key1", 2L, 200L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 2L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 2L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 4L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 4L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 4L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 9L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 4L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 9L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 6L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 11L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 9L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 9L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 16L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 13L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 16L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 14L)));
+
+ List<RowData> actualRows = testHarness.extractOutputValues();
+
+ validateRows(actualRows, expectedRows);
+ }
+
+ @Test
+ public void testRetractWithEarlyOut() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 0L, 100L));
+ testHarness.processElement(insertRecord("key1", 0L, 101L));
+ testHarness.processElement(insertRecord("key1", 0L, 102L));
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 2L, 201L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 5L, 502L));
+ testHarness.processElement(insertRecord("key1", 5L, 501L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(updateBeforeRecord("key1", 0L, 100L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 0L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 0L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 0L, 101L),
+ GenericRowData.ofKind(RowKind.INSERT, 0L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 0L, 102L),
+ GenericRowData.ofKind(RowKind.INSERT, 0L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 1L, 100L),
+ GenericRowData.ofKind(RowKind.INSERT, 1L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 3L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 200L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 2L, 201L),
+ GenericRowData.ofKind(RowKind.INSERT, 5L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 10L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 500L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_BEFORE,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 15L)),
+ new JoinedRowData(
+ RowKind.UPDATE_AFTER,
+ GenericRowData.ofKind(
+ RowKind.UPDATE_AFTER,
+ StringData.fromString("key1"),
+ 5L,
+ 502L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 5L, 501L),
+ GenericRowData.ofKind(RowKind.INSERT, 20L)),
+ new JoinedRowData(
+ GenericRowData.ofKind(
+ RowKind.INSERT, StringData.fromString("key1"), 6L, 600L),
+ GenericRowData.ofKind(RowKind.INSERT, 26L)),
+ new JoinedRowData(
+ RowKind.UPDATE_BEFORE,
Review Comment:
Fixed the TODO and also updated the tests
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1957701908
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,689 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.StateTtlConfig;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+import static org.apache.flink.table.runtime.util.StateConfigUtil.createTtlConfig;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunction<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final long stateRetentionTime;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
Review Comment:
Renamed
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,689 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.StateTtlConfig;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+import static org.apache.flink.table.runtime.util.StateConfigUtil.createTtlConfig;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunction<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final long stateRetentionTime;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+ @VisibleForTesting protected transient ValueStateDescriptor<Long> idStateDescriptor;
+
+ // state to hold a sorted list each containing a tuple of sort key and list of IDs
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+
+ @VisibleForTesting
+ protected transient ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>
+ sortedListStateDescriptor;
+
+ // state to hold ID and its associated input row until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<Long, RowData> valueStateDescriptor;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+ @VisibleForTesting protected transient MapStateDescriptor<RowData, RowData> accStateDescriptor;
+
+ protected transient AggsHandleFunction aggFuncs;
+
+ // Metrics
+ private static final String IDS_NOT_FOUND_METRIC_NAME = "numOfIdsNotFound";
+ private transient Counter numOfIdsNotFound;
+ private static final String SORT_KEYS_NOT_FOUND_METRIC_NAME = "numOfSortKeysNotFound";
+ private transient Counter numOfSortKeysNotFound;
+
+ @VisibleForTesting
+ protected Counter getNumOfIdsNotFound() {
+ return numOfIdsNotFound;
+ }
+
+ @VisibleForTesting
+ protected Counter getNumOfSortKeysNotFound() {
+ return numOfSortKeysNotFound;
+ }
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long stateRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
Review Comment:
Renamed
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1957702535
##########
flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunctionTest.java:
##########
@@ -0,0 +1,2028 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.streaming.api.operators.KeyedProcessOperator;
+import org.apache.flink.streaming.api.watermark.Watermark;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordComparator;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.BigIntType;
+import org.apache.flink.table.types.logical.IntType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.VarCharType;
+import org.apache.flink.table.utils.HandwrittenSelectorUtil;
+import org.apache.flink.types.RowKind;
+
+import org.junit.Test;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.insertRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateAfterRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateBeforeRecord;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for {@link NonTimeRangeUnboundedPrecedingFunction}. */
+public class NonTimeRangeUnboundedPrecedingFunctionTest extends RowTimeOverWindowTestBase {
+
+ private static final int SORT_KEY_IDX = 1;
+
+ private static final LogicalType[] SORT_KEY_TYPES = new LogicalType[] {new BigIntType()};
+
+ private static final InternalTypeInfo<RowData> INPUT_ROW_TYPE =
+ InternalTypeInfo.ofFields(VarCharType.STRING_TYPE, new BigIntType(), new IntType());
+
+ private static final RowDataKeySelector SORT_KEY_SELECTOR =
+ HandwrittenSelectorUtil.getRowDataSelector(
+ new int[] {SORT_KEY_IDX}, INPUT_ROW_TYPE.toRowFieldTypes());
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR_ASC =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(0, true);
+ }
+ };
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR_DESC =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(0, false);
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_ROW_VALUE_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ return new TestRowValueEqualiser();
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_SORT_KEY_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ return new TestSortKeyEqualiser();
+ }
+ };
+
+ /** Custom test comparator for comparing numbers. */
+ public static class LongRecordComparator implements RecordComparator {
+
+ private int pos;
+ private boolean isAscending;
+
+ public LongRecordComparator(int pos, boolean isAscending) {
+ this.pos = pos;
+ this.isAscending = isAscending;
+ }
+
+ @Override
+ public int compare(RowData o1, RowData o2) {
+ boolean null0At1 = o1.isNullAt(pos);
+ boolean null0At2 = o2.isNullAt(pos);
+ int cmp0 =
+ null0At1 && null0At2
+ ? 0
+ : (null0At1
+ ? -1
+ : (null0At2
+ ? 1
+ : Long.compare(o1.getLong(pos), o2.getLong(pos))));
+ if (cmp0 != 0) {
+ if (isAscending) {
+ return cmp0;
+ } else {
+ return -cmp0;
+ }
+ }
+ return 0;
+ }
+ }
+
+ /** Custom test row equaliser for comparing rows. */
+ public static class TestRowValueEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getString(0).equals(row2.getString(0))
+ && row1.getLong(1) == row2.getLong(1)
+ && row1.getLong(2) == row2.getLong(2);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ /** Custom test sortKey equaliser for comparing sort keys. */
+ public static class TestSortKeyEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getLong(0) == row2.getLong(0);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ @Test
+ public void testInsertOnlyRecordsWithCustomSortKey() throws Exception {
+ NonTimeRangeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeRangeUnboundedPrecedingFunction<RowData>(
+ 0,
+ aggsHandleFunction,
+ GENERATED_ROW_VALUE_EQUALISER,
+ GENERATED_SORT_KEY_EQUALISER,
+ GENERATED_SORT_KEY_COMPARATOR_ASC,
+ accTypes,
+ inputFieldTypes,
+ SORT_KEY_TYPES,
+ SORT_KEY_SELECTOR) {};
+ KeyedProcessOperator<RowData, RowData, RowData> operator =
+ new KeyedProcessOperator<>(function);
+
+ OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
+ createTestHarness(operator);
+
+ testHarness.open();
+
+ // put some records
+ testHarness.processElement(insertRecord("key1", 1L, 100L));
+ testHarness.processElement(insertRecord("key1", 2L, 200L));
+ testHarness.processElement(insertRecord("key1", 5L, 500L));
+ testHarness.processElement(insertRecord("key1", 6L, 600L));
+ testHarness.processElement(insertRecord("key2", 1L, 100L));
+ testHarness.processElement(insertRecord("key2", 2L, 200L));
+
+ testHarness.processWatermark(new Watermark(500L));
+
+ // out of order record should trigger updates for all records after its inserted position
+ testHarness.processElement(insertRecord("key1", 4L, 400L));
+
+ List<RowData> expectedRows =
+ Arrays.asList(
+ new JoinedRowData(
Review Comment:
Refactored
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1907587503
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ insertIntoSortedListOptimized(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ removeFromSortedListOptimized(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
Review Comment:
The new implementation stores accumulators per row
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1907359357
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ insertIntoSortedListOptimized(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ removeFromSortedListOptimized(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
Review Comment:
This has been implemented in the new logic
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Add NonTimeRangeUnboundedPrecedingFunction for OverAggregate [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r2012508906
##########
flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecOverAggregate.java:
##########
@@ -183,15 +190,15 @@ protected Transformation<RowData> translateToPlanInternal(
final int orderKey = orderKeys[0];
final LogicalType orderKeyType = inputRowType.getFields().get(orderKey).getType();
- // check time field && identify window rowtime attribute
- final int rowTimeIdx;
+ // check time field && identify window time attribute
+ TimeAttribute timeAttribute;
if (isRowtimeAttribute(orderKeyType)) {
- rowTimeIdx = orderKey;
+ timeAttribute = TimeAttribute.ROW_TIME;
} else if (isProctimeAttribute(orderKeyType)) {
- rowTimeIdx = -1;
+ timeAttribute = TimeAttribute.PROC_TIME;
} else {
- throw new TableException(
- "OVER windows' ordering in stream mode must be defined on a time attribute.");
+ timeAttribute = TimeAttribute.NON_TIME;
+ LOG.info("Non-time attribute window detected");
Review Comment:
Removed the extraneous log line
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "flinkbot (via GitHub)" <gi...@apache.org>.
flinkbot commented on PR #25753:
URL: https://github.com/apache/flink/pull/25753#issuecomment-2521880092
<!--
Meta data
{
"version" : 1,
"metaDataEntries" : [ {
"hash" : "169870dd6e3c0b6e36530b9d639a14fbba7e061e",
"status" : "UNKNOWN",
"url" : "TBD",
"triggerID" : "169870dd6e3c0b6e36530b9d639a14fbba7e061e",
"triggerType" : "PUSH"
} ]
}-->
## CI report:
* 169870dd6e3c0b6e36530b9d639a14fbba7e061e UNKNOWN
<details>
<summary>Bot commands</summary>
The @flinkbot bot supports the following commands:
- `@flinkbot run azure` re-run the last Azure build
</details>
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "davidradl (via GitHub)" <gi...@apache.org>.
davidradl commented on PR #25753:
URL: https://github.com/apache/flink/pull/25753#issuecomment-2541622743
Reviewed by Chi on 12/12/24. appears that this PR is healthily progressing
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1931388443
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
Review Comment:
Added the logic to early out in the new implementation.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928129091
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
Review Comment:
Method no longer exists.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1928137538
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,482 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ //private transient MapState<RowData, List<Integer>> sortIndexState;
+ private transient MapState<Long, List<Long>> sortIndexState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ // Initialize state which maintains the sorted key Index
+ ListTypeInfo<Long> listTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ // TODO: Key for the map should be RowData which can contain multiple sort keys
+ MapStateDescriptor<Long, List<Long>> sortedKeyIndexStateDescriptor =
+ new MapStateDescriptor<Long, List<Long>>("sortedKeyIndexState", Types.LONG, listTypeInfo);
+ sortIndexState = getRuntimeContext().getMapState(sortedKeyIndexStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (sortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ isInserted = true;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ setAccumulatorOfPrevRow(sortedKeyList, iterator.previousIndex());
+ }
+ insertRow(sortedKeyList, iterator, id, inputKey, input);
+ }
+
+ List<Long> ids = sortIndexState.get(inputKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, id, out);
+ } else {
+ sendUpdate(input, origRowKind, out);
+ }
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ ids = sortIndexState.get(curKey.getLong(1));
+ if (ids.size() > 1) {
+ updateSameSortKeys(iterator, ids, -1L, out);
+ } else {
+ if (!updateRow(curKey, out)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setAccumulatorOfPrevRow(List<RowData> sortedKeyList, int prevIndex)
+ throws Exception {
+ if (prevIndex < 0) {
+ currFunction.createAccumulators();
+ } else {
+ Long prevKey = sortedKeyList.get(prevIndex).getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ }
+ }
+
+ private void insertRow(
+ List<RowData> sortedKeyList,
+ ListIterator<RowData> iterator,
+ Long id,
+ RowData inputKey,
+ RowData input)
+ throws Exception {
+ iterator.add(inputKey);
+ // Only accumulate input with currFunction
+ currFunction.accumulate(input);
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ sortedKeyState.update(sortedKeyList);
+ List<Long> sortedIds = sortIndexState.get(inputKey.getLong(1));
+ if (sortedIds == null || sortedIds.isEmpty()) {
+ sortedIds = new ArrayList<>();
+ }
+ sortedIds.add(id);
+ sortIndexState.put(inputKey.getLong(1), sortedIds);
+ idState.update(++id);
+ }
+
+
+ private void sendUpdate(RowData value, RowKind rowKind, Collector<RowData> out) throws Exception {
+ output.setRowKind(rowKind);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private void sendUpdateBefore(RowData value, RowData prevAcc, Collector<RowData> out) {
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(value, prevAcc);
+ out.collect(output);
+ }
+
+ private void sendUpdateAfter(RowData value, Collector<RowData> out) throws Exception {
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(value, currFunction.getValue());
+ out.collect(output);
+ }
+
+ private boolean updateRow(RowData curKey, Collector<RowData> out) throws Exception {
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getAccumulators())) {
Review Comment:
The logic to early out is still missing in the new implementation but aiming for correctness before I can add this optimization.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "fhueske (via GitHub)" <gi...@apache.org>.
fhueske commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1880101103
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
Review Comment:
what is stored in `accState`?
Is it the accumulators for the last record in the ordered list?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ insertIntoSortedListOptimized(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ removeFromSortedListOptimized(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
Review Comment:
move this out of the while loop?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ insertIntoSortedListOptimized(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ removeFromSortedListOptimized(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
Review Comment:
If we store the accumulators, we can early out this loop as soon as the accumulators before and after the change are identical. Since the accumulator for row x depends on the accumulator of row x-1 and x itself and x didn't change, we can stop traversing the list once the accumulators are identical before and after the row was inserted.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ insertIntoSortedListOptimized(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ removeFromSortedListOptimized(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
Review Comment:
I just realized that the Map approach won't helps if we can have to access all elements for any update (insert or delete). We can either persist the accumulators for all input rows (so we only need to recompute all accumulators after the inserted/deleted row) or if we split the input row into the fields that are needed for the aggregate computation and those that aren't
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
Review Comment:
`accState` is never updated.
I don't think we need to have accumulator state the way it is used here.
Storing the accumulators for each input row would reduce the computational cost for updates.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ insertIntoSortedListOptimized(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ removeFromSortedListOptimized(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
Review Comment:
we could early out the loop once the accumulators before and after the change are the same (see detailed comment above)
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
Review Comment:
if we don't persist the accumulators for all input rows, another optimization would be to split the input row data into two maps, one for the fields that are needed to compute the aggregates and one for those that are simply forwarded.
Then we could recompute the accumulators by just deserializing the first map.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ insertIntoSortedListOptimized(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ removeFromSortedListOptimized(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
Review Comment:
If we would store the accumulators for all input rows, we could check if the aggregates really changed.
For example a `MIN()`, `MAX()`, `LAG()`, or `LEAD()` function might not change, even if we inserted or deleted a row. We should only emit updates if something really changed.
Any change we filter out here, will reduce the processing overhead in subsequent operators / sinks.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ insertIntoSortedListOptimized(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ removeFromSortedListOptimized(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
Review Comment:
If we persist per-row accumulators, we could check if the rows differ and only emit updates in that case.
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ insertIntoSortedListOptimized(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ removeFromSortedListOptimized(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListOptimized(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
Review Comment:
it would be better if we wouldn't need to rely on retractable accumulators.
They are often less space and/or compute efficient.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1942258579
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeRangeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,578 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/** A basic implementation to support non-time range unbounded over aggregate with retract mode. */
+public class NonTimeRangeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeRangeUnboundedPrecedingFunction.class);
+
+ private final GeneratedAggsHandleFunction generatedAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordEqualiser generatedSortKeyEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+
+ protected final KeySelector<RowData, RowData> sortKeySelector;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser valueEqualiser;
+ private transient RecordEqualiser sortKeyEqualiser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ private final LogicalType[] sortKeyTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a sorted list each containing a tuple of sort key with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<Tuple2<RowData, List<Long>>>> sortedListState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold sortKey and its associated accumulator
+ private transient MapState<RowData, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+
+ public NonTimeRangeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordEqualiser genSortKeyEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ LogicalType[] sortKeyTypes,
+ RowDataKeySelector sortKeySelector) {
+ super(minRetentionTime, maxRetentionTime);
+ this.generatedAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedSortKeyEqualiser = genSortKeyEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyTypes = sortKeyTypes;
+ this.sortKeySelector = sortKeySelector;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction =
+ generatedAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize value/row equaliser
+ valueEqualiser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sortKey equaliser
+ sortKeyEqualiser =
+ generatedSortKeyEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize state to maintain id counter
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputRowTypeInfo = InternalTypeInfo.ofFields(inputFieldTypes);
+ InternalTypeInfo<RowData> sortKeyRowTypeInfo = InternalTypeInfo.ofFields(this.sortKeyTypes);
+
+ // Initialize state which maintains a sorted list of tuples(sortKey, List of IDs)
+ ListTypeInfo<Long> idListTypeInfo = new ListTypeInfo<Long>(Types.LONG);
+ ListTypeInfo<Tuple2<RowData, List<Long>>> listTypeInfo =
+ new ListTypeInfo<>(new TupleTypeInfo<>(sortKeyRowTypeInfo, idListTypeInfo));
+ ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>> sortedListStateDescriptor =
+ new ValueStateDescriptor<List<Tuple2<RowData, List<Long>>>>(
+ "sortedListState", listTypeInfo);
+ sortedListState = getRuntimeContext().getState(sortedListStateDescriptor);
+
+ // Initialize state which maintains the actual row
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>(
+ "valueMapState", Types.LONG, inputRowTypeInfo);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ // Initialize accumulator state per row
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ MapStateDescriptor<RowData, RowData> accStateDescriptor =
+ new MapStateDescriptor<RowData, RowData>(
+ "accMapState", sortKeyRowTypeInfo, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits a DELETE for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element. Emits the same aggregated value for all elements with the same sortKey to comply
+ * with the sql RANGE syntax.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ RowKind rowKind = input.getRowKind();
+
+ switch (rowKind) {
+ case INSERT:
+ case UPDATE_AFTER:
+ insertIntoSortedList(input, out);
+ break;
+
+ case DELETE:
+ case UPDATE_BEFORE:
+ removeFromSortedList(input, out);
+ break;
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ currFunction.cleanup();
+ }
+
+ /**
+ * Adds a new element(input) to a sortedList. The sortedList contains a list of Tuple<SortKey,
+ * List<Long Ids>> Extracts the inputSortKey from the input and compares it with every element
+ * in the sortedList If an sortKey already exists in the sortedList for the input, add the id to
+ * the list of ids and update the sortedList Otherwise find the right position in the sortedList
+ * and add a new entry in the middle. If no matching sortKey is found, add a new entry in the
+ * end of the sortedList.
Review Comment:
Fixed javadoc
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1908093769
##########
flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunctionTest.java:
##########
@@ -0,0 +1,467 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.streaming.api.operators.KeyedProcessOperator;
+import org.apache.flink.streaming.api.watermark.Watermark;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordComparator;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.types.RowKind;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.insertRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateAfterRecord;
+import static org.apache.flink.table.runtime.util.StreamRecordUtils.updateBeforeRecord;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for {@link NonTimeUnboundedPrecedingFunction}. */
+public class NonTimeUnboundedPrecedingFunctionTest extends RowTimeOverWindowTestBase {
+
+ private static final int SORT_KEY_IDX = 1;
+
+ private static final FieldGetter SORT_KEY_FIELD_GETTER =
+ RowData.createFieldGetter(DataTypes.BIGINT().getLogicalType(), SORT_KEY_IDX);
+
+ private static final GeneratedRecordComparator GENERATED_SORT_KEY_COMPARATOR =
+ new GeneratedRecordComparator("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordComparator newInstance(ClassLoader classLoader) {
+ return new LongRecordComparator(SORT_KEY_IDX);
+ }
+ };
+
+ private static final GeneratedRecordEqualiser GENERATED_RECORD_EQUALISER =
+ new GeneratedRecordEqualiser("", "", new Object[0]) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public RecordEqualiser newInstance(ClassLoader classLoader) {
+ // return new RowDataRecordEqualiser();
+ return new TestRecordEqualiser();
+ }
+ };
+
+ /** Custom test comparator for comparing numbers. */
+ public static class LongRecordComparator implements RecordComparator {
+
+ private int pos;
+
+ public LongRecordComparator(int pos) {
+ this.pos = pos;
+ }
+
+ @Override
+ public int compare(RowData o1, RowData o2) {
+ boolean null0At1 = o1.isNullAt(pos);
+ boolean null0At2 = o2.isNullAt(pos);
+ int cmp0 =
+ null0At1 && null0At2
+ ? 0
+ : (null0At1
+ ? -1
+ : (null0At2
+ ? 1
+ : Long.compare(o1.getLong(pos), o2.getLong(pos))));
+ if (cmp0 != 0) {
+ return cmp0;
+ }
+ return 0;
+ }
+ }
+
+ /** Custom test row equaliser for comparing rows. */
+ public static class TestRecordEqualiser implements RecordEqualiser {
+
+ private static final long serialVersionUID = -6706336100425614942L;
+
+ @Override
+ public boolean equals(RowData row1, RowData row2) {
+ if (row1 instanceof BinaryRowData && row2 instanceof BinaryRowData) {
+ return row1.equals(row2);
+ } else if (row1 instanceof GenericRowData && row2 instanceof GenericRowData) {
+ return row1.getString(0).equals(row2.getString(0))
+ && row1.getLong(1) == row2.getLong(1)
+ && row1.getLong(2) == row2.getLong(2);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ @Test
+ public void testInsertOnlyRecordsWithCustomSortKey() throws Exception {
+ NonTimeUnboundedPrecedingFunction<RowData> function =
+ new NonTimeUnboundedPrecedingFunction<RowData>(
+ 0,
+ 2000,
+ aggsHandleFunction,
Review Comment:
Added a test for AVG as a restore test
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1908079190
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+
+ currFunction.accumulate(curValue);
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ RowData curAcc = accMapState.get(curKey.getLong(keyIdx));
+ currFunction.setAccumulators(curAcc);
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+
+ valueMapState.remove(curKey.getLong(keyIdx));
+ accMapState.remove(curKey.getLong(keyIdx));
+ if (iterator.hasPrevious()) {
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ RowData prevAcc = accMapState.get(prev.getLong(keyIdx));
+ currFunction.setAccumulators(prevAcc);
+ } else {
+ currFunction.setAccumulators(currFunction.createAccumulators());
+ }
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
Review Comment:
Moved to `insertRow` helper function
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1907343095
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
Review Comment:
Good to know this!
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1908080468
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,591 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+ // state to hold row ID and its associated accumulator
+ private transient MapState<Long, RowData> accMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> accStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("accMapState", Types.LONG, accTypeInfo);
+ accMapState = getRuntimeContext().getMapState(accStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
+ if (lastAccumulatorCurr == null) {
+ // initialize accumulator
+ lastAccumulatorCurr = currFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ currFunction.setAccumulators(lastAccumulatorCurr);
+
+ // get last accumulator
+ RowData lastAccumulatorPrev = accState.value();
+ if (lastAccumulatorPrev == null) {
+ // initialize accumulator
+ lastAccumulatorPrev = prevFunction.createAccumulators();
+ }
+ // set accumulator in function context first
+ prevFunction.setAccumulators(lastAccumulatorPrev);
+
+ RowKind rowKind = input.getRowKind();
+
+ if (rowKind == RowKind.INSERT || rowKind == RowKind.UPDATE_AFTER) {
+ // insertIntoSortedList(input, out);
+ // insertIntoSortedListWithValueMap(input, out);
+ insertIntoSortedListWithValueMapAndAccumulatorMap(input, out);
+ } else if (rowKind == RowKind.DELETE || rowKind == RowKind.UPDATE_BEFORE) {
+ // removeFromSortedList(input, out);
+ // removeFromSortedListWithValueMap(input, out);
+ removeFromSortedListWithValueMapAndAccumulatorMap(input, out);
+ }
+
+ // Reset acc state since we can have out of order inserts into the ordered list
+ currFunction.resetAccumulators();
+ prevFunction.resetAccumulators();
+ currFunction.cleanup();
+ prevFunction.cleanup();
+ }
+
+ private void insertIntoSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ List<RowData> rowList = inputState.value();
+ if (rowList == null) {
+ rowList = new ArrayList<>();
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = rowData.getRowKind();
+ rowData.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ if (sortKeyComparator.compare(curRow, rowData) > 0) {
+ iterator.previous();
+ iterator.add(rowData);
+ isInserted = true;
+ break;
+ }
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(rowData);
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(rowData);
+
+ // Update state with the newly inserted row
+ inputState.update(rowList);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ isInserted = true;
+ id++;
+ break;
+ }
+ // Can also add the accKey to the sortedKeyList to avoid reading from the valueMapState
+ RowData curRow = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ id++;
+ }
+
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ }
+ }
+
+ private void insertIntoSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ if (sortedKeyList == null) {
+ sortedKeyList = new ArrayList<>();
+ }
+ Long id = idState.value();
+ if (id == null) {
+ id = 0L;
+ }
+ boolean isInserted = false;
+ RowKind origRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT);
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ RowData inputKey = GenericRowData.of(-1L, sortKeyFieldGetter.getFieldOrNull(input));
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next(); // (ID, sortKey)
+ if (newSortKeyComparator.compare(curKey, inputKey) > 0) {
+ iterator.previous();
+ RowData prev = sortedKeyList.get(iterator.previousIndex());
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ Long prevKey = prev.getLong(keyIdx);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ currFunction.accumulate(input);
+ accMapState.put(id, currFunction.getValue());
+ isInserted = true;
+ id++;
+ break;
+ }
+ }
+
+ // Add to the end of the list
+ if (!isInserted) {
+ if (iterator.hasPrevious()) {
+ RowData prev = iterator.previous();
+ Long prevKey = prev.getLong(0);
+ RowData prevAcc = accMapState.get(prevKey);
+ currFunction.setAccumulators(prevAcc);
+ iterator.next();
+ }
+ // Only accumulate rowData with currFunction
+ currFunction.accumulate(input);
+ iterator.add(GenericRowData.of(id, sortKeyFieldGetter.getFieldOrNull(input)));
+ valueMapState.put(id, input);
+ accMapState.put(id, currFunction.getAccumulators());
+ id++;
+ }
+
+ // Update sorted key state with the newly inserted row's key
+ sortedKeyState.update(sortedKeyList);
+ idState.update(id);
+
+ // prepare output row
+ output.setRowKind(origRowKind);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+
+ // Emit updated agg value for all records after newly inserted row
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
+ // If previously accumulated value is same as the newly accumulated value
+ // then no need to send updates downstream since nothing has changed
+ break;
+ }
+ // Generate UPDATE_BEFORE
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevAcc);
+ out.collect(output);
+ // Generate UPDATE_AFTER
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+
+ accMapState.put(curKey.getLong(keyIdx), currFunction.getValue());
+ }
+ }
+
+ private void removeFromSortedList(RowData rowData, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ rowData.setRowKind(RowKind.INSERT);
+ List<RowData> rowList = inputState.value();
+ ListIterator<RowData> iterator = rowList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curRow = iterator.next();
+ currFunction.accumulate(curRow);
+ prevFunction.accumulate(curRow);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curRow, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curRow, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curRow, rowData)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(rowData, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ currFunction.retract(curRow);
+ isRetracted = true;
+ }
+ }
+
+ // Update state without the retracted row
+ inputState.update(rowList);
+ }
+
+ private void removeFromSortedListWithValueMap(RowData input, Collector<RowData> out)
+ throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ currFunction.accumulate(curValue);
+ prevFunction.accumulate(curValue);
+ if (isRetracted) {
+ // Emit updated agg value for all records after retraction
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(curValue, prevFunction.getValue());
+ out.collect(output);
+
+ output.setRowKind(RowKind.UPDATE_AFTER);
+ output.replace(curValue, currFunction.getValue());
+ out.collect(output);
+ } else if (equaliser.equals(curValue, input)) {
+ // Retract record
+ output.setRowKind(RowKind.UPDATE_BEFORE);
+ output.replace(input, currFunction.getValue());
+ out.collect(output);
+ iterator.remove();
+ valueMapState.remove(curKey.getLong(keyIdx));
+ currFunction.retract(curValue);
+ isRetracted = true;
+ }
+ }
+
+ // Update sorted key state without the retracted row
+ sortedKeyState.update(sortedKeyList);
+ }
+
+ private void removeFromSortedListWithValueMapAndAccumulatorMap(
+ RowData input, Collector<RowData> out) throws Exception {
+ boolean isRetracted = false;
+ input.setRowKind(RowKind.INSERT);
+ List<RowData> sortedKeyList = sortedKeyState.value();
+ ListIterator<RowData> iterator = sortedKeyList.listIterator();
+
+ while (iterator.hasNext()) {
+ RowData curKey = iterator.next();
+ RowData curValue = valueMapState.get(curKey.getLong(keyIdx));
+ if (isRetracted) {
+ RowData prevAcc = accMapState.get(curKey.getLong(keyIdx));
+ if (prevAcc.equals(currFunction.getValue())) {
Review Comment:
Fixed
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] [FLINK-19059]: Support non-time retract mode for OverAggregate operator [flink]
Posted by "bvarghese1 (via GitHub)" <gi...@apache.org>.
bvarghese1 commented on code in PR #25753:
URL: https://github.com/apache/flink/pull/25753#discussion_r1908082519
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/over/NonTimeUnboundedPrecedingFunction.java:
##########
@@ -0,0 +1,445 @@
+/*
+ * 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.flink.table.runtime.operators.over;
+
+import org.apache.flink.api.common.functions.OpenContext;
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.ValueState;
+import org.apache.flink.api.common.state.ValueStateDescriptor;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.java.typeutils.ListTypeInfo;
+import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.RowData.FieldGetter;
+import org.apache.flink.table.data.utils.JoinedRowData;
+import org.apache.flink.table.runtime.dataview.PerKeyStateDataViewStore;
+import org.apache.flink.table.runtime.functions.KeyedProcessFunctionWithCleanupState;
+import org.apache.flink.table.runtime.generated.AggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedAggsHandleFunction;
+import org.apache.flink.table.runtime.generated.GeneratedRecordComparator;
+import org.apache.flink.table.runtime.generated.GeneratedRecordEqualiser;
+import org.apache.flink.table.runtime.generated.RecordEqualiser;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.Collector;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+
+/** A basic implementation to support non-time unbounded over aggregate with retract mode. */
+public class NonTimeUnboundedPrecedingFunction<K>
+ extends KeyedProcessFunctionWithCleanupState<K, RowData, RowData> {
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(NonTimeUnboundedPrecedingFunction.class);
+
+ private final int keyIdx;
+ private final int sortKeyIdx;
+ private final FieldGetter sortKeyFieldGetter;
+
+ private final GeneratedAggsHandleFunction genAggsHandler;
+ private final GeneratedRecordEqualiser generatedRecordEqualiser;
+ private final GeneratedRecordComparator generatedRecordComparator;
+ private final GeneratedRecordComparator keyGeneratedRecordComparator;
+
+ // The util to compare two rows based on the sort attribute.
+ private transient Comparator<RowData> sortKeyComparator;
+ private transient Comparator<RowData> newSortKeyComparator;
+ // The record equaliser used to equal RowData.
+ private transient RecordEqualiser equaliser;
+
+ private final LogicalType[] accTypes;
+ private final LogicalType[] inputFieldTypes;
+ protected transient JoinedRowData output;
+
+ // state to hold the accumulators of the aggregations
+ private transient ValueState<RowData> accState;
+ // state to hold rows until state ttl expires
+ private transient ValueState<List<RowData>> inputState;
+
+ // state to hold the Long ID counter
+ private transient ValueState<Long> idState;
+
+ // state to hold a list of sorted keys with an artificial id
+ // The artificial id acts as the key in the valueMapState
+ private transient ValueState<List<RowData>> sortedKeyState;
+ // state to hold rows until state ttl expires
+ private transient MapState<Long, RowData> valueMapState;
+
+ protected transient AggsHandleFunction currFunction;
+ protected transient AggsHandleFunction prevFunction;
+
+ public NonTimeUnboundedPrecedingFunction(
+ long minRetentionTime,
+ long maxRetentionTime,
+ GeneratedAggsHandleFunction genAggsHandler,
+ GeneratedRecordEqualiser genRecordEqualiser,
+ GeneratedRecordComparator genRecordComparator,
+ GeneratedRecordComparator keyGenRecordComparator,
+ LogicalType[] accTypes,
+ LogicalType[] inputFieldTypes,
+ FieldGetter sortKeyFieldGetter,
+ int sortKeyIdx) {
+ super(minRetentionTime, maxRetentionTime);
+ this.genAggsHandler = genAggsHandler;
+ this.generatedRecordEqualiser = genRecordEqualiser;
+ this.generatedRecordComparator = genRecordComparator;
+ this.keyGeneratedRecordComparator = keyGenRecordComparator;
+ this.accTypes = accTypes;
+ this.inputFieldTypes = inputFieldTypes;
+ this.sortKeyFieldGetter = sortKeyFieldGetter;
+ this.sortKeyIdx = sortKeyIdx;
+ this.keyIdx = 0;
+ }
+
+ @Override
+ public void open(OpenContext openContext) throws Exception {
+ // Initialize agg functions
+ currFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ currFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ prevFunction = genAggsHandler.newInstance(getRuntimeContext().getUserCodeClassLoader());
+ prevFunction.open(new PerKeyStateDataViewStore(getRuntimeContext()));
+
+ // Initialize output record
+ output = new JoinedRowData();
+
+ // Initialize record equaliser
+ equaliser =
+ generatedRecordEqualiser.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize sort comparator
+ sortKeyComparator =
+ generatedRecordComparator.newInstance(getRuntimeContext().getUserCodeClassLoader());
+
+ newSortKeyComparator =
+ keyGeneratedRecordComparator.newInstance(
+ getRuntimeContext().getUserCodeClassLoader());
+
+ // Initialize accumulator state
+ InternalTypeInfo<RowData> accTypeInfo = InternalTypeInfo.ofFields(accTypes);
+ ValueStateDescriptor<RowData> accStateDesc =
+ new ValueStateDescriptor<RowData>("accState", accTypeInfo);
+ accState = getRuntimeContext().getState(accStateDesc);
+
+ // Input elements are all binary rows as they came from network
+ InternalTypeInfo<RowData> inputType = InternalTypeInfo.ofFields(inputFieldTypes);
+ ListTypeInfo<RowData> rowListTypeInfo = new ListTypeInfo<RowData>(inputType);
+
+ ValueStateDescriptor<List<RowData>> inputStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("inputState", rowListTypeInfo);
+
+ // Initialize state which maintains records in sorted(ASC) order
+ inputState = getRuntimeContext().getState(inputStateDescriptor);
+
+ ValueStateDescriptor<Long> idStateDescriptor =
+ new ValueStateDescriptor<Long>("idState", Long.class);
+ idState = getRuntimeContext().getState(idStateDescriptor);
+
+ LogicalType idType = DataTypes.BIGINT().getLogicalType();
+ LogicalType sortKeyType = inputType.toRowType().getTypeAt(sortKeyIdx);
+ RowType sortedRow = RowType.of(idType, sortKeyType);
+ LogicalType[] sortedRowTypes = sortedRow.getChildren().toArray(new LogicalType[0]);
+ InternalTypeInfo<RowData> sortedKeyRowType = InternalTypeInfo.ofFields(sortedRowTypes);
+ ListTypeInfo<RowData> sortedKeyTypeInfo = new ListTypeInfo<>(sortedKeyRowType);
+ // Initialize state which maintains a sorted list of pair(ID, sortKey)
+ ValueStateDescriptor<List<RowData>> sortedKeyStateDescriptor =
+ new ValueStateDescriptor<List<RowData>>("sortedKeyState", sortedKeyTypeInfo);
+ sortedKeyState = getRuntimeContext().getState(sortedKeyStateDescriptor);
+
+ MapStateDescriptor<Long, RowData> valueStateDescriptor =
+ new MapStateDescriptor<Long, RowData>("valueState", Types.LONG, inputType);
+ valueMapState = getRuntimeContext().getMapState(valueStateDescriptor);
+
+ initCleanupTimeState("NonTimeUnboundedPrecedingFunctionCleanupTime");
+ }
+
+ /**
+ * Puts an element from the input stream into state. Emits the aggregated value for the newly
+ * inserted element. For append stream emits updates(UB, UA) for all elements which are present
+ * after the newly inserted element. For retract stream emits an UB for the element and
+ * thereafter emits updates(UB, UA) for all elements which are present after the retracted
+ * element.
+ *
+ * @param input The input value.
+ * @param ctx A {@link Context} that allows querying the timestamp of the element and getting
+ * TimerService for registering timers and querying the time. The context is only valid
+ * during the invocation of this method, do not store it.
+ * @param out The collector for returning result values.
+ * @throws Exception
+ */
+ @Override
+ public void processElement(
+ RowData input,
+ KeyedProcessFunction<K, RowData, RowData>.Context ctx,
+ Collector<RowData> out)
+ throws Exception {
+ // register state-cleanup timer
+ registerProcessingCleanupTimer(ctx, ctx.timerService().currentProcessingTime());
+
+ // get last accumulator
+ RowData lastAccumulatorCurr = accState.value();
Review Comment:
Removed accState since its not used.
--
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: issues-unsubscribe@flink.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org