This is an automated email from the ASF dual-hosted git repository.

twalthr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/master by this push:
     new 2b71c2f8965 [FLINK-41065][table] Allow process table functions to 
exclude state entries per call
2b71c2f8965 is described below

commit 2b71c2f89651763059a082a1df2c740b66e3e421
Author: Timo Walther <[email protected]>
AuthorDate: Mon Oct 26 09:42:13 2026 +0100

    [FLINK-41065][table] Allow process table functions to exclude state entries 
per call
    
    This closes #29566.
---
 .../table/types/inference/StateTypeStrategy.java   |  27 ++++-
 .../table/types/inference/TypeInferenceUtil.java   |  51 +++++----
 .../stream/ProcessTableFunctionSemanticTests.java  |   1 +
 .../stream/ProcessTableFunctionTestPrograms.java   |  23 ++++
 .../exec/stream/ProcessTableFunctionTestUtils.java |  76 +++++++++++++
 .../functions/ptf/BuiltInProcessTableFunction.java | 119 ++++++++++++++++++---
 .../functions/ProcessTableFunctionTestHarness.java |   6 +-
 .../ProcessTableFunctionTestHarnessTest.java       |  69 ++++++++++++
 8 files changed, 331 insertions(+), 41 deletions(-)

diff --git 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/StateTypeStrategy.java
 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/StateTypeStrategy.java
index 9936c6d4d56..88108906466 100644
--- 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/StateTypeStrategy.java
+++ 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/StateTypeStrategy.java
@@ -19,13 +19,25 @@
 package org.apache.flink.table.types.inference;
 
 import org.apache.flink.annotation.PublicEvolving;
+import org.apache.flink.table.functions.ProcessTableFunction;
+import org.apache.flink.table.types.DataType;
 
 import javax.annotation.Nullable;
 
 import java.time.Duration;
 import java.util.Optional;
 
-/** Strategy for inferring a function call's intermediate result data type 
(i.e. state entry). */
+/**
+ * Strategy for inferring a function call's intermediate result data type 
(i.e. state entry).
+ *
+ * <p>For {@link ProcessTableFunction}, returning {@link Optional#empty()} 
from {@link
+ * #inferType(CallContext)} excludes the state entry from a specific call. A 
function's {@code
+ * eval()} and {@code onTimer()} methods must not declare a parameter for it. 
This allows declaring
+ * a superset of state entries while the strategy decides what a call needs.
+ *
+ * <p>For aggregating functions, returning {@link Optional#empty()} from {@link
+ * #inferType(CallContext)} is not supported as the accumulator must always be 
inferable.
+ */
 @PublicEvolving
 public interface StateTypeStrategy extends TypeStrategy {
 
@@ -42,6 +54,19 @@ public interface StateTypeStrategy extends TypeStrategy {
         return new DefaultStateTypeStrategy(typeStrategy, timeToLive, 
broadcast);
     }
 
+    /**
+     * Infers the data type of the state entry from the given function call.
+     *
+     * <p>For {@link ProcessTableFunction}, returning {@link Optional#empty()} 
means that the state
+     * entry is not used by this call. It is then excluded from the call's 
state, and the function's
+     * {@code eval()} and {@code onTimer()} methods must not declare a 
parameter for it.
+     *
+     * <p>For aggregating functions, the accumulator must always be inferable. 
Returning {@link
+     * Optional#empty()} fails with a validation exception.
+     */
+    @Override
+    Optional<DataType> inferType(CallContext callContext);
+
     /**
      * The time-to-live (TTL) duration that automatically cleans up the state 
entry.
      *
diff --git 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/TypeInferenceUtil.java
 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/TypeInferenceUtil.java
index fa3598c8273..a2360dedcfd 100644
--- 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/TypeInferenceUtil.java
+++ 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/TypeInferenceUtil.java
@@ -23,6 +23,7 @@ import org.apache.flink.table.api.TableException;
 import org.apache.flink.table.api.ValidationException;
 import org.apache.flink.table.catalog.DataTypeFactory;
 import org.apache.flink.table.functions.FunctionDefinition;
+import org.apache.flink.table.functions.FunctionKind;
 import org.apache.flink.table.functions.ModelSemantics;
 import org.apache.flink.table.functions.TableSemantics;
 import org.apache.flink.table.functions.UserDefinedFunctionHelper;
@@ -36,7 +37,6 @@ import javax.annotation.Nullable;
 import java.time.Duration;
 import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.stream.Collectors;
@@ -183,21 +183,20 @@ public final class TypeInferenceUtil {
     /**
      * Infers {@link StateInfo}s using the given {@link StateTypeStrategy}s. 
It assumes that input
      * arguments have been adapted before if necessary.
+     *
+     * <p>State entries whose strategy returns {@link Optional#empty()} are 
excluded from the
+     * result. This allows a function to declare more state entries than a 
specific call needs. Only
+     * process table functions can exclude state entries; for all other 
functions every state entry,
+     * such as the accumulator of an aggregating function, must be inferred.
      */
     public static LinkedHashMap<String, StateInfo> inferStateInfos(
             CallContext callContext, LinkedHashMap<String, StateTypeStrategy> 
stateTypeStrategies) {
-        return stateTypeStrategies.entrySet().stream()
-                .map(
-                        e ->
-                                Map.entry(
-                                        e.getKey(),
-                                        inferStateInfo(callContext, 
e.getKey(), e.getValue())))
-                .collect(
-                        Collectors.toMap(
-                                Map.Entry::getKey,
-                                Map.Entry::getValue,
-                                (x, y) -> y,
-                                LinkedHashMap::new));
+        final LinkedHashMap<String, StateInfo> stateInfos = new 
LinkedHashMap<>();
+        stateTypeStrategies.forEach(
+                (name, strategy) ->
+                        inferStateInfo(callContext, name, strategy)
+                                .ifPresent(stateInfo -> stateInfos.put(name, 
stateInfo)));
+        return stateInfos;
     }
 
     /** Generates a signature of the given {@link FunctionDefinition}. */
@@ -583,24 +582,32 @@ public final class TypeInferenceUtil {
         return castCallContext;
     }
 
-    private static StateInfo inferStateInfo(
+    private static Optional<StateInfo> inferStateInfo(
             CallContext callContext, String name, StateTypeStrategy 
stateTypeStrategy) {
         final DataType stateType = 
stateTypeStrategy.inferType(callContext).orElse(null);
-        if (stateType == null || isUnknown(stateType)) {
-            final String errorMessage;
+        if (stateType == null) {
+            if (callContext.getFunctionDefinition().getKind() == 
FunctionKind.PROCESS_TABLE) {
+                return Optional.empty();
+            }
             if 
(name.equals(UserDefinedFunctionHelper.DEFAULT_ACCUMULATOR_NAME)) {
-                errorMessage = "Could not infer an accumulator type for the 
given arguments.";
-            } else {
-                errorMessage =
-                        String.format("Could not infer a data type for state 
entry '%s'.", name);
+                throw new ValidationException(
+                        "Could not infer an accumulator type for the given 
arguments.");
             }
-            throw new ValidationException(errorMessage);
+            throw new ValidationException(
+                    String.format("Could not infer a data type for state entry 
'%s'.", name));
+        }
+        if (isUnknown(stateType)) {
+            throw new ValidationException(
+                    String.format(
+                            "Could not infer a data type for state entry '%s'. 
"
+                                    + "Untyped NULL received.",
+                            name));
         }
 
         final Duration ttl = 
stateTypeStrategy.getTimeToLive(callContext).orElse(null);
         final boolean broadcast = stateTypeStrategy.isBroadcast();
 
-        return new StateInfo(stateType, ttl, broadcast);
+        return Optional.of(new StateInfo(stateType, ttl, broadcast));
     }
 
     private static boolean isUnknown(DataType dataType) {
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionSemanticTests.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionSemanticTests.java
index 1faf06beb60..44713492267 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionSemanticTests.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionSemanticTests.java
@@ -86,6 +86,7 @@ public class ProcessTableFunctionSemanticTests extends 
SemanticTestBase {
                 
ProcessTableFunctionTestPrograms.PROCESS_OPTIONAL_PARTITION_BY_TIME,
                 ProcessTableFunctionTestPrograms.PROCESS_OPTIONAL_ON_TIME,
                 ProcessTableFunctionTestPrograms.PROCESS_POJO_STATE_TIME,
+                ProcessTableFunctionTestPrograms.PROCESS_OPTIONAL_STATE,
                 
ProcessTableFunctionTestPrograms.PROCESS_EAGER_AND_VALUE_VIEW_STATE_TIME,
                 ProcessTableFunctionTestPrograms.PROCESS_CHAINED_TIME,
                 
ProcessTableFunctionTestPrograms.PROCESS_CHAINED_TIME_TABLE_API,
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionTestPrograms.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionTestPrograms.java
index d2512eb0e54..d4ce7d8ab2b 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionTestPrograms.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionTestPrograms.java
@@ -61,6 +61,7 @@ import 
org.apache.flink.table.planner.plan.nodes.exec.stream.ProcessTableFunctio
 import 
org.apache.flink.table.planner.plan.nodes.exec.stream.ProcessTableFunctionTestUtils.NotifyStatefulSetsRulesFunction;
 import 
org.apache.flink.table.planner.plan.nodes.exec.stream.ProcessTableFunctionTestUtils.OptionalOnTimeFunction;
 import 
org.apache.flink.table.planner.plan.nodes.exec.stream.ProcessTableFunctionTestUtils.OptionalPartitionOnTimeFunction;
+import 
org.apache.flink.table.planner.plan.nodes.exec.stream.ProcessTableFunctionTestUtils.OptionalStateFunction;
 import 
org.apache.flink.table.planner.plan.nodes.exec.stream.ProcessTableFunctionTestUtils.OrderByFunction;
 import 
org.apache.flink.table.planner.plan.nodes.exec.stream.ProcessTableFunctionTestUtils.PojoArgsFunction;
 import 
org.apache.flink.table.planner.plan.nodes.exec.stream.ProcessTableFunctionTestUtils.PojoCreatingFunction;
@@ -1462,6 +1463,28 @@ public class ProcessTableFunctionTestPrograms {
                             "INSERT INTO sink SELECT * FROM f(r => TABLE t 
PARTITION BY name, on_time => DESCRIPTOR(ts))")
                     .build();
 
+    public static final TableTestProgram PROCESS_OPTIONAL_STATE =
+            TableTestProgram.of(
+                            "process-optional-state",
+                            "state entries excluded by the type inference 
depending on the call")
+                    .setupTemporarySystemFunction("f", 
OptionalStateFunction.class)
+                    .setupTableSource(TIMED_SOURCE)
+                    .setupTableSink(
+                            SinkTestStep.newBuilder("sink")
+                                    .addSchema(KEYED_BASE_SINK_SCHEMA)
+                                    .consumedValues(
+                                            "+I[Bob, {+I[6]}]",
+                                            "+I[Alice, {+I[1]}]",
+                                            "+I[Bob, {+I[6], +I[6]}]",
+                                            "+I[Alice, {+I[1], +I[1]}]")
+                                    .build())
+                    .runSql(
+                            "INSERT INTO sink "
+                                    + "SELECT name, `out` FROM f(r => TABLE t 
PARTITION BY name, mode => 1, uid => 'one', on_time => DESCRIPTOR(ts)) "
+                                    + "UNION ALL "
+                                    + "SELECT name, `out` FROM f(r => TABLE t 
PARTITION BY name, mode => 2, uid => 'two', on_time => DESCRIPTOR(ts))")
+                    .build();
+
     public static final TableTestProgram 
PROCESS_EAGER_AND_VALUE_VIEW_STATE_TIME =
             TableTestProgram.of(
                             "process-eager-and-value-view-state-time",
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionTestUtils.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionTestUtils.java
index 1122c322a0e..eaf3a27f661 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionTestUtils.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionTestUtils.java
@@ -39,6 +39,7 @@ import org.apache.flink.table.functions.ScalarFunction;
 import org.apache.flink.table.functions.TableSemantics;
 import 
org.apache.flink.table.runtime.operators.process.AbstractProcessTableOperator.RunnerContext;
 import org.apache.flink.table.test.program.SourceTestStep;
+import org.apache.flink.table.types.inference.StateTypeStrategy;
 import org.apache.flink.table.types.inference.StaticArgument;
 import org.apache.flink.table.types.inference.StaticArgumentTrait;
 import org.apache.flink.table.types.inference.TypeInference;
@@ -55,6 +56,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -843,6 +845,80 @@ public class ProcessTableFunctionTestUtils {
         }
     }
 
+    /**
+     * Testing function that declares two state entries but only uses the 
second one if the {@code
+     * mode} argument is 2. The state type strategy of the unused entry 
returns {@link
+     * Optional#empty()}, such that {@code eval()} and {@code onTimer()} are 
resolved to the
+     * overload without a parameter for it. Both overloads count the processed 
rows and emit the
+     * state once at the end of the input.
+     */
+    public static class OptionalStateFunction extends 
AppendProcessTableFunctionBase {
+        @Override
+        public TypeInference getTypeInference(DataTypeFactory typeFactory) {
+            final LinkedHashMap<String, StateTypeStrategy> stateStrategies = 
new LinkedHashMap<>();
+            stateStrategies.put(
+                    "s1",
+                    StateTypeStrategy.of(
+                            callContext ->
+                                    Optional.of(
+                                            DataTypes.ROW(DataTypes.FIELD("i", 
DataTypes.INT())))));
+            stateStrategies.put(
+                    "s2",
+                    StateTypeStrategy.of(
+                            callContext ->
+                                    callContext.getArgumentValue(1, 
Integer.class).orElse(1) == 2
+                                            ? Optional.of(
+                                                    DataTypes.ROW(
+                                                            DataTypes.FIELD(
+                                                                    "s", 
DataTypes.STRING())))
+                                            : Optional.empty()));
+            return TypeInference.newBuilder()
+                    .staticArguments(
+                            StaticArgument.table(
+                                    "r",
+                                    Row.class,
+                                    false,
+                                    EnumSet.of(
+                                            
StaticArgumentTrait.SET_SEMANTIC_TABLE,
+                                            
StaticArgumentTrait.REQUIRE_ON_TIME)),
+                            StaticArgument.scalar("mode", DataTypes.INT(), 
false))
+                    .stateTypeStrategies(stateStrategies)
+                    .outputTypeStrategy(
+                            callContext ->
+                                    Optional.of(
+                                            DataTypes.ROW(
+                                                    DataTypes.FIELD("out", 
DataTypes.STRING()))))
+                    .build();
+        }
+
+        public void eval(Context ctx, Row s1, Row r, Integer mode) {
+            mutateFirstState(ctx, s1);
+        }
+
+        public void eval(Context ctx, Row s1, Row s2, Row r, Integer mode) {
+            mutateFirstState(ctx, s1);
+            s2.setField("s", String.valueOf(s1.<Integer>getFieldAs("i")));
+        }
+
+        public void onTimer(OnTimerContext ctx, Row s1) {
+            collectObjects(s1);
+        }
+
+        public void onTimer(OnTimerContext ctx, Row s1, Row s2) {
+            collectObjects(s1, s2);
+        }
+
+        private void mutateFirstState(Context ctx, Row s1) {
+            final Integer i = s1.getFieldAs("i");
+            if (i == null) {
+                ctx.timeContext(Long.class).registerOnTime("end", 
Long.MAX_VALUE);
+                s1.setField("i", 1);
+            } else {
+                s1.setField("i", i + 1);
+            }
+        }
+    }
+
     /**
      * Testing function that combines eager value state (a POJO) and a {@link 
ValueView} and
      * accesses both from eval() and onTimer().
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/ptf/BuiltInProcessTableFunction.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/ptf/BuiltInProcessTableFunction.java
index 1ac2e2e03e7..5cecc79866f 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/ptf/BuiltInProcessTableFunction.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/ptf/BuiltInProcessTableFunction.java
@@ -19,21 +19,32 @@
 package org.apache.flink.table.runtime.functions.ptf;
 
 import org.apache.flink.annotation.Internal;
+import org.apache.flink.table.api.dataview.ListView;
+import org.apache.flink.table.api.dataview.MapView;
+import org.apache.flink.table.api.dataview.ValueView;
 import org.apache.flink.table.catalog.DataTypeFactory;
 import org.apache.flink.table.functions.BuiltInFunctionDefinition;
 import org.apache.flink.table.functions.FunctionRequirement;
 import org.apache.flink.table.functions.ProcessTableFunction;
 import org.apache.flink.table.functions.SpecializedFunction.SpecializedContext;
+import org.apache.flink.table.runtime.dataview.DataViewUtils;
 import org.apache.flink.table.types.DataType;
 import org.apache.flink.table.types.inference.CallContext;
 import org.apache.flink.table.types.inference.StateTypeStrategy;
 import org.apache.flink.table.types.inference.TypeInference;
 import org.apache.flink.table.types.inference.TypeStrategies;
+import org.apache.flink.table.types.logical.LogicalType;
 import org.apache.flink.table.types.utils.DataTypeUtils;
 
+import javax.annotation.Nullable;
+
+import java.time.Duration;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
+import java.util.function.UnaryOperator;
 import java.util.stream.Collectors;
 
 /**
@@ -59,6 +70,18 @@ public abstract class BuiltInProcessTableFunction<T> extends 
ProcessTableFunctio
 
     protected BuiltInProcessTableFunction(
             final BuiltInFunctionDefinition definition, final 
SpecializedContext context) {
+        this(definition, context, null);
+    }
+
+    /**
+     * Creates a function whose state entries are declared by the specialized 
function itself
+     * instead of the definition. Use {@link #internalState(Map.Entry[])} or 
{@link
+     * #customState(Map.Entry[])} to create the strategies.
+     */
+    protected BuiltInProcessTableFunction(
+            final BuiltInFunctionDefinition definition,
+            final SpecializedContext context,
+            @Nullable final LinkedHashMap<String, StateTypeStrategy> 
internalStateStrategies) {
         this.definition = definition;
         final CallContext callContext = context.getCallContext();
         final TypeInference definitionTypeInference =
@@ -76,7 +99,85 @@ public abstract class BuiltInProcessTableFunction<T> extends 
ProcessTableFunctio
                         .orElseThrow(IllegalStateException::new);
 
         this.internalStateStrategies =
-                toInternalStateStrategies(definitionTypeInference, 
callContext);
+                internalStateStrategies != null
+                        ? internalStateStrategies
+                        : toInternalStateStrategies(definitionTypeInference);
+    }
+
+    /**
+     * Declares the state entries of a specialized function in order. The 
strategies may infer
+     * external data types, they are converted to internal ones.
+     */
+    @SafeVarargs
+    protected static LinkedHashMap<String, StateTypeStrategy> internalState(
+            final Map.Entry<String, StateTypeStrategy>... entries) {
+        return state(BuiltInProcessTableFunction::toInternalStateStrategy, 
entries);
+    }
+
+    /**
+     * Declares the state entries of a specialized function in order. The 
strategies must already
+     * infer the data types that the function works on, no internal conversion 
is applied.
+     */
+    @SafeVarargs
+    protected static LinkedHashMap<String, StateTypeStrategy> customState(
+            final Map.Entry<String, StateTypeStrategy>... entries) {
+        return state(UnaryOperator.identity(), entries);
+    }
+
+    private static LinkedHashMap<String, StateTypeStrategy> state(
+            final UnaryOperator<StateTypeStrategy> mapper,
+            final Map.Entry<String, StateTypeStrategy>[] entries) {
+        final LinkedHashMap<String, StateTypeStrategy> result = new 
LinkedHashMap<>();
+        for (final Map.Entry<String, StateTypeStrategy> entry : entries) {
+            result.put(entry.getKey(), mapper.apply(entry.getValue()));
+        }
+        return result;
+    }
+
+    private static StateTypeStrategy toInternalStateStrategy(final 
StateTypeStrategy strategy) {
+        return new StateTypeStrategy() {
+            @Override
+            public Optional<DataType> inferType(final CallContext callContext) 
{
+                return strategy.inferType(callContext)
+                        .map(BuiltInProcessTableFunction::toInternalStateType);
+            }
+
+            @Override
+            public Optional<Duration> getTimeToLive(final CallContext 
callContext) {
+                return strategy.getTimeToLive(callContext);
+            }
+
+            @Override
+            public boolean isBroadcast() {
+                return strategy.isBroadcast();
+            }
+        };
+    }
+
+    /**
+     * Converts a state type to its internal representation. For {@link 
ValueView}/{@link
+     * MapView}/{@link ListView} only the value/key/element types are 
internalized; the view wrapper
+     * is preserved, unlike {@link DataTypeUtils#toInternalDataType} which 
would rewrite the whole
+     * view to {@code RowData}.
+     */
+    private static DataType toInternalStateType(final DataType stateType) {
+        final LogicalType logicalType = stateType.getLogicalType();
+        if (DataViewUtils.isDataView(logicalType, ValueView.class)) {
+            final DataType valueType = stateType.getChildren().get(0);
+            return 
ValueView.newValueViewDataType(DataTypeUtils.toInternalDataType(valueType));
+        }
+        if (DataViewUtils.isDataView(logicalType, MapView.class)) {
+            final DataType mapType = stateType.getChildren().get(0);
+            return MapView.newMapViewDataType(
+                    
DataTypeUtils.toInternalDataType(mapType.getChildren().get(0)),
+                    
DataTypeUtils.toInternalDataType(mapType.getChildren().get(1)));
+        }
+        if (DataViewUtils.isDataView(logicalType, ListView.class)) {
+            final DataType arrayType = stateType.getChildren().get(0);
+            return ListView.newListViewDataType(
+                    
DataTypeUtils.toInternalDataType(arrayType.getChildren().get(0)));
+        }
+        return DataTypeUtils.toInternalDataType(stateType);
     }
 
     // Uses deprecated typedArguments() because staticArguments() does not 
apply the conversion
@@ -95,23 +196,13 @@ public abstract class BuiltInProcessTableFunction<T> 
extends ProcessTableFunctio
                 .build();
     }
 
-    /** Wraps each state type strategy to produce internal data types, 
preserving TTL. */
+    /** Wraps each state type strategy to produce internal data types. */
     private static LinkedHashMap<String, StateTypeStrategy> 
toInternalStateStrategies(
-            final TypeInference definitionTypeInference, final CallContext 
callContext) {
+            final TypeInference definitionTypeInference) {
         final LinkedHashMap<String, StateTypeStrategy> result = new 
LinkedHashMap<>();
         definitionTypeInference
                 .getStateTypeStrategies()
-                .forEach(
-                        (name, strategy) ->
-                                result.put(
-                                        name,
-                                        StateTypeStrategy.of(
-                                                ctx ->
-                                                        strategy.inferType(ctx)
-                                                                .map(
-                                                                        
DataTypeUtils
-                                                                               
 ::toInternalDataType),
-                                                
strategy.getTimeToLive(callContext).orElse(null))));
+                .forEach((name, strategy) -> result.put(name, 
toInternalStateStrategy(strategy)));
         return result;
     }
 
diff --git 
a/flink-table/flink-table-test-utils/src/main/java/org/apache/flink/table/runtime/functions/ProcessTableFunctionTestHarness.java
 
b/flink-table/flink-table-test-utils/src/main/java/org/apache/flink/table/runtime/functions/ProcessTableFunctionTestHarness.java
index 5204ad57868..b3efb9ef31e 100644
--- 
a/flink-table/flink-table-test-utils/src/main/java/org/apache/flink/table/runtime/functions/ProcessTableFunctionTestHarness.java
+++ 
b/flink-table/flink-table-test-utils/src/main/java/org/apache/flink/table/runtime/functions/ProcessTableFunctionTestHarness.java
@@ -1756,10 +1756,8 @@ public class ProcessTableFunctionTestHarness<OUT> 
implements AutoCloseable {
 
                 Optional<DataType> dataTypeOpt = 
strategy.inferType(callContext);
                 if (dataTypeOpt.isEmpty()) {
-                    throw new IllegalStateException(
-                            String.format(
-                                    "Could not infer data type for state 
parameter '%s'",
-                                    stateName));
+                    // state entry is not used by this call
+                    continue;
                 }
                 DataType stateDataType = dataTypeOpt.get();
 
diff --git 
a/flink-table/flink-table-test-utils/src/test/java/org/apache/flink/table/runtime/functions/ProcessTableFunctionTestHarnessTest.java
 
b/flink-table/flink-table-test-utils/src/test/java/org/apache/flink/table/runtime/functions/ProcessTableFunctionTestHarnessTest.java
index 6e079414bee..ddd279a58cd 100644
--- 
a/flink-table/flink-table-test-utils/src/test/java/org/apache/flink/table/runtime/functions/ProcessTableFunctionTestHarnessTest.java
+++ 
b/flink-table/flink-table-test-utils/src/test/java/org/apache/flink/table/runtime/functions/ProcessTableFunctionTestHarnessTest.java
@@ -28,10 +28,15 @@ import org.apache.flink.table.api.ValidationException;
 import org.apache.flink.table.api.dataview.ListView;
 import org.apache.flink.table.api.dataview.MapView;
 import org.apache.flink.table.api.dataview.ValueView;
+import org.apache.flink.table.catalog.DataTypeFactory;
 import org.apache.flink.table.connector.ChangelogMode;
 import org.apache.flink.table.functions.ProcessTableFunction;
 import org.apache.flink.table.functions.TableSemantics;
 import 
org.apache.flink.table.runtime.functions.ProcessTableFunctionTestHarness.TableArgument;
+import org.apache.flink.table.types.inference.StateTypeStrategy;
+import org.apache.flink.table.types.inference.StaticArgument;
+import org.apache.flink.table.types.inference.StaticArgumentTrait;
+import org.apache.flink.table.types.inference.TypeInference;
 import org.apache.flink.types.Row;
 import org.apache.flink.types.RowKind;
 
@@ -42,9 +47,12 @@ import java.time.Duration;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -495,6 +503,43 @@ class ProcessTableFunctionTestHarnessTest {
         }
     }
 
+    /** PTF that declares two state entries of which the type inference 
excludes the second one. */
+    public static class PTFWithExcludedState extends ProcessTableFunction<Row> 
{
+        @Override
+        public TypeInference getTypeInference(DataTypeFactory typeFactory) {
+            final LinkedHashMap<String, StateTypeStrategy> stateStrategies = 
new LinkedHashMap<>();
+            stateStrategies.put(
+                    "counter",
+                    StateTypeStrategy.of(
+                            callContext ->
+                                    Optional.of(
+                                            DataTypes.ROW(
+                                                    DataTypes.FIELD("count", 
DataTypes.INT())))));
+            stateStrategies.put("unused", StateTypeStrategy.of(callContext -> 
Optional.empty()));
+            return TypeInference.newBuilder()
+                    .staticArguments(
+                            StaticArgument.table(
+                                    "input",
+                                    Row.class,
+                                    false,
+                                    
EnumSet.of(StaticArgumentTrait.SET_SEMANTIC_TABLE)))
+                    .stateTypeStrategies(stateStrategies)
+                    .outputTypeStrategy(
+                            callContext ->
+                                    Optional.of(
+                                            DataTypes.ROW(
+                                                    DataTypes.FIELD("count", 
DataTypes.INT()))))
+                    .build();
+        }
+
+        public void eval(Row counter, Row input) {
+            final Integer count = counter.getFieldAs("count");
+            final int newCount = count == null ? 1 : count + 1;
+            counter.setField("count", newCount);
+            collect(Row.of(newCount));
+        }
+    }
+
     // 
-------------------------------------------------------------------------
     // Builder Configuration Tests
     // 
-------------------------------------------------------------------------
@@ -1822,6 +1867,30 @@ class ProcessTableFunctionTestHarnessTest {
         harness.close();
     }
 
+    @Test
+    void testStateEntryExcludedByTypeInference() throws Exception {
+        ProcessTableFunctionTestHarness<Row> harness =
+                
ProcessTableFunctionTestHarness.ofClass(PTFWithExcludedState.class)
+                        .withTableArgument(
+                                TableArgument.forName("input")
+                                        .type(DataTypes.of("ROW<key STRING, 
value INT>"))
+                                        .partitionBy("key")
+                                        .build())
+                        .build();
+
+        harness.processElementForTable("input", Row.of("A", 10));
+        harness.processElementForTable("input", Row.of("A", 20));
+        harness.processElementForTable("input", Row.of("B", 5));
+
+        assertThat(harness.getOutput())
+                .containsExactly(Row.of("A", 1), Row.of("A", 2), Row.of("B", 
1));
+
+        final Row counterA = harness.getStateForKey("counter", Row.of("A"));
+        assertThat(counterA.<Integer>getFieldAs("count")).isEqualTo(2);
+
+        harness.close();
+    }
+
     @Test
     void testInitialStateWithListView() throws Exception {
         ListView<Integer> initialList = new ListView<>();

Reply via email to