twalthr commented on code in PR #27928:
URL: https://github.com/apache/flink/pull/27928#discussion_r3093175093


##########
docs/content/docs/dev/table/testing/ptf_test_harness.md:
##########
@@ -0,0 +1,361 @@
+---
+title: "Testing Process Table Functions"
+weight: 10
+type: docs
+---
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Testing Process Table Functions

Review Comment:
   Can we move this to:
   
https://nightlies.apache.org/flink/flink-docs-master/docs/dev/table/functions/ptfs/
   
   I know this page is already long, but it is also very nice to everything you 
need to know around PTFs in one page.



##########
docs/content/docs/dev/table/testing/ptf_test_harness.md:
##########
@@ -0,0 +1,361 @@
+---
+title: "Testing Process Table Functions"
+weight: 10
+type: docs
+---
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Testing Process Table Functions
+
+The `ProcessTableFunctionTestHarness` provides a lightweight unit testing 
framework for Process Table
+Functions (PTFs). It allows you to test PTF logic without starting a full 
Flink cluster. It is useful
+for testing and validating PTF business logic, multi-table PTF behaviour and 
validating errors.
+
+For end-to-end integration testing with the full Flink planner and runtime, 
use integration tests
+instead.
+
+{{< top >}}
+
+## Quick Start
+
+{{< tabs "quickstart" >}}
+{{< tab "Java" >}}
+```java
+import org.apache.flink.table.annotation.*;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.functions.ProcessTableFunction;
+import 
org.apache.flink.table.runtime.functions.ProcessTableFunctionTestHarness;
+import org.apache.flink.types.Row;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+// PTF under test
+@DataTypeHint("ROW<doubled INT>")
+public class DoublePTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row 
input) {
+        int value = input.getFieldAs("value");
+        collect(Row.of(value * 2));
+    }
+}
+
+// Test
+@Test
+void testDoublePTF() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(DoublePTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .build()) {
+
+        harness.processElement(Row.of(5));
+        harness.processElement(Row.of(10));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).hasSize(2);
+        assertThat(output.get(0)).isEqualTo(Row.of(10));
+        assertThat(output.get(1)).isEqualTo(Row.of(20));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+{{< top >}}
+
+## Common Testing Scenarios
+
+### Testing Row-Semantic Tables
+
+Use `.withTableArgument()` to configure the input table schema:
+
+{{< tabs "row-semantic" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<value INT>")
+public class PassthroughPTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row 
input) {
+        collect(input);
+    }
+}
+
+@Test
+void testPassthrough() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(PassthroughPTF.class)

Review Comment:
   nit: use two spaces in examples to keep the page readable and lines short



##########
docs/content/docs/dev/table/testing/ptf_test_harness.md:
##########
@@ -0,0 +1,361 @@
+---
+title: "Testing Process Table Functions"
+weight: 10
+type: docs
+---
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Testing Process Table Functions
+
+The `ProcessTableFunctionTestHarness` provides a lightweight unit testing 
framework for Process Table
+Functions (PTFs). It allows you to test PTF logic without starting a full 
Flink cluster. It is useful
+for testing and validating PTF business logic, multi-table PTF behaviour and 
validating errors.
+
+For end-to-end integration testing with the full Flink planner and runtime, 
use integration tests
+instead.
+
+{{< top >}}
+
+## Quick Start
+
+{{< tabs "quickstart" >}}
+{{< tab "Java" >}}
+```java
+import org.apache.flink.table.annotation.*;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.functions.ProcessTableFunction;
+import 
org.apache.flink.table.runtime.functions.ProcessTableFunctionTestHarness;
+import org.apache.flink.types.Row;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+// PTF under test
+@DataTypeHint("ROW<doubled INT>")
+public class DoublePTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row 
input) {
+        int value = input.getFieldAs("value");
+        collect(Row.of(value * 2));
+    }
+}
+
+// Test
+@Test
+void testDoublePTF() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(DoublePTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .build()) {
+
+        harness.processElement(Row.of(5));
+        harness.processElement(Row.of(10));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).hasSize(2);
+        assertThat(output.get(0)).isEqualTo(Row.of(10));
+        assertThat(output.get(1)).isEqualTo(Row.of(20));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+{{< top >}}
+
+## Common Testing Scenarios
+
+### Testing Row-Semantic Tables
+
+Use `.withTableArgument()` to configure the input table schema:
+
+{{< tabs "row-semantic" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<value INT>")
+public class PassthroughPTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row 
input) {
+        collect(input);
+    }
+}
+
+@Test
+void testPassthrough() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(PassthroughPTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .build()) {
+
+        harness.processElement(Row.of(42));
+        harness.processElement(Row.of(100));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).containsExactly(Row.of(42), Row.of(100));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing Set-Semantic Tables with Partitioning
+
+For `SET_SEMANTIC_TABLE`, use `.withPartitionBy()` to configure partition 
columns:
+
+{{< tabs "set-semantic" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<doubled INT>")
+public class PartitionedPTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row 
input) {
+        int value = input.getFieldAs("value");
+        collect(Row.of(value * 2));
+    }
+}
+
+@Test
+void testPartitionedPTF() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(PartitionedPTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<key STRING, 
value INT>"))
+                    .withPartitionBy("input", "key")
+                    .build()) {
+
+        harness.processElement(Row.of("A", 10));
+        harness.processElement(Row.of("B", 20));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output.get(0)).isEqualTo(Row.of("A", 20));
+        assertThat(output.get(1)).isEqualTo(Row.of("B", 40));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing Multiple Table Arguments
+
+Use `processElementForTable()` to specify which table receives each row:
+
+{{< tabs "multi-table" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<output STRING>")
+public class JoinPTF extends ProcessTableFunction<Row> {
+    public void eval(
+            @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row left,
+            @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row right) {
+        if (left != null) {
+            collect(Row.of("LEFT: " + left));
+        }
+        if (right != null) {
+            collect(Row.of("RIGHT: " + right));
+        }
+    }
+}
+
+@Test
+void testMultiTable() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(JoinPTF.class)
+                    .withTableArgument("left", DataTypes.of("ROW<id INT, name 
STRING>"))
+                    .withPartitionBy("left", "id")
+                    .withTableArgument("right", DataTypes.of("ROW<id INT, city 
STRING>"))
+                    .withPartitionBy("right", "id")
+                    .build()) {
+
+        // Use processElementForTable() to target specific tables
+        harness.processElementForTable("left", Row.of(1, "Alice"));
+        harness.processElementForTable("right", Row.of(1, "Berlin"));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output.get(0)).isEqualTo(Row.of(1, null, "LEFT: +I[1, 
Alice]"));
+        assertThat(output.get(1)).isEqualTo(Row.of(null, 1, "RIGHT: +I[1, 
Berlin]"));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing with Scalar Arguments
+
+Use `.withScalarArgument()` to configure scalar parameter values:
+
+{{< tabs "scalar-args" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<value INT>")
+public class FilterPTF extends ProcessTableFunction<Row> {

Review Comment:
   nit: do we also want to update at least one of the examples to non-Row. Just 
to show that also `ProcessTableFunction<Integer>` in this example is possible 
and is auto wrapped in the output.



##########
docs/content/docs/dev/table/testing/ptf_test_harness.md:
##########
@@ -0,0 +1,361 @@
+---
+title: "Testing Process Table Functions"
+weight: 10
+type: docs
+---
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Testing Process Table Functions
+
+The `ProcessTableFunctionTestHarness` provides a lightweight unit testing 
framework for Process Table
+Functions (PTFs). It allows you to test PTF logic without starting a full 
Flink cluster. It is useful

Review Comment:
   PTF can also run locally without a full Flink cluster. This is more for unit 
testing.
   ```suggestion
   Functions (PTFs). It allows you to unit test PTF logic. It is useful
   ```



##########
flink-table/flink-table-test-utils/src/main/java/org/apache/flink/table/runtime/functions/DataTypeFactoryMock.java:
##########
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.functions;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.base.VoidSerializer;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.api.ValidationException;
+import org.apache.flink.table.catalog.DataTypeFactory;
+import org.apache.flink.table.catalog.UnresolvedIdentifier;
+import org.apache.flink.table.types.AbstractDataType;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.UnresolvedDataType;
+import org.apache.flink.table.types.extraction.DataTypeExtractor;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.utils.LogicalTypeParser;
+import org.apache.flink.table.types.utils.TypeInfoDataTypeConverter;
+
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** {@link DataTypeFactory} mock for testing purposes. Copied from 
flink-table-common. */
+@Internal
+class DataTypeFactoryMock implements DataTypeFactory {
+
+    public Optional<DataType> dataType = Optional.empty();
+
+    public Optional<LogicalType> logicalType = Optional.empty();
+
+    public Optional<Class<?>> expectedClass = Optional.empty();

Review Comment:
   There is a lot of unnecessary code. How about we call this 
`TestHarnessDataTypeFactory` and clean it up. We should inform the user if 
certain methods are not implemented. Also `dummyRaw` and `dummySerializer` are 
not needed.



##########
flink-table/flink-table-test-utils/src/main/java/org/apache/flink/table/runtime/functions/ProcessTableFunctionTestHarness.java:
##########
@@ -0,0 +1,1199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.functions;
+
+import org.apache.flink.annotation.PublicEvolving;
+import org.apache.flink.table.annotation.ArgumentTrait;
+import org.apache.flink.table.annotation.StateHint;
+import org.apache.flink.table.catalog.DataTypeFactory;
+import org.apache.flink.table.data.conversion.DataStructureConverter;

Review Comment:
   Important: The harness should be a light-weight Maven module that only 
depends on `flink-table-common`. Currently this module has a full planner and 
runtime dependency. How about we repurpose this module and potentially move 
`TableAssertionTest` to the planner module. After that we can simplify the deps 
for test-utils. This also means that DataStructureConverter is not available 
anymore. For this, let's introduce a new module 
`flink-table/flink-table-type-utils`. Introducing this module was under 
discussion a couple of times, we should do it now. Ideally under a new Jira 
ticket that gets merged before this PR.



##########
docs/content/docs/dev/table/testing/ptf_test_harness.md:
##########
@@ -0,0 +1,361 @@
+---
+title: "Testing Process Table Functions"
+weight: 10
+type: docs
+---
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Testing Process Table Functions
+
+The `ProcessTableFunctionTestHarness` provides a lightweight unit testing 
framework for Process Table
+Functions (PTFs). It allows you to test PTF logic without starting a full 
Flink cluster. It is useful
+for testing and validating PTF business logic, multi-table PTF behaviour and 
validating errors.
+
+For end-to-end integration testing with the full Flink planner and runtime, 
use integration tests
+instead.
+
+{{< top >}}
+
+## Quick Start
+
+{{< tabs "quickstart" >}}
+{{< tab "Java" >}}
+```java
+import org.apache.flink.table.annotation.*;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.functions.ProcessTableFunction;
+import 
org.apache.flink.table.runtime.functions.ProcessTableFunctionTestHarness;
+import org.apache.flink.types.Row;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+// PTF under test
+@DataTypeHint("ROW<doubled INT>")
+public class DoublePTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row 
input) {
+        int value = input.getFieldAs("value");
+        collect(Row.of(value * 2));
+    }
+}
+
+// Test
+@Test
+void testDoublePTF() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(DoublePTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .build()) {
+
+        harness.processElement(Row.of(5));
+        harness.processElement(Row.of(10));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).hasSize(2);
+        assertThat(output.get(0)).isEqualTo(Row.of(10));
+        assertThat(output.get(1)).isEqualTo(Row.of(20));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+{{< top >}}
+
+## Common Testing Scenarios
+
+### Testing Row-Semantic Tables
+
+Use `.withTableArgument()` to configure the input table schema:
+
+{{< tabs "row-semantic" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<value INT>")
+public class PassthroughPTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row 
input) {
+        collect(input);
+    }
+}
+
+@Test
+void testPassthrough() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(PassthroughPTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .build()) {
+
+        harness.processElement(Row.of(42));
+        harness.processElement(Row.of(100));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).containsExactly(Row.of(42), Row.of(100));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing Set-Semantic Tables with Partitioning
+
+For `SET_SEMANTIC_TABLE`, use `.withPartitionBy()` to configure partition 
columns:
+
+{{< tabs "set-semantic" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<doubled INT>")
+public class PartitionedPTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row 
input) {
+        int value = input.getFieldAs("value");
+        collect(Row.of(value * 2));
+    }
+}
+
+@Test
+void testPartitionedPTF() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(PartitionedPTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<key STRING, 
value INT>"))
+                    .withPartitionBy("input", "key")
+                    .build()) {
+
+        harness.processElement(Row.of("A", 10));
+        harness.processElement(Row.of("B", 20));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output.get(0)).isEqualTo(Row.of("A", 20));
+        assertThat(output.get(1)).isEqualTo(Row.of("B", 40));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing Multiple Table Arguments
+
+Use `processElementForTable()` to specify which table receives each row:
+
+{{< tabs "multi-table" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<output STRING>")
+public class JoinPTF extends ProcessTableFunction<Row> {
+    public void eval(
+            @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row left,
+            @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row right) {
+        if (left != null) {
+            collect(Row.of("LEFT: " + left));
+        }
+        if (right != null) {
+            collect(Row.of("RIGHT: " + right));
+        }
+    }
+}
+
+@Test
+void testMultiTable() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(JoinPTF.class)
+                    .withTableArgument("left", DataTypes.of("ROW<id INT, name 
STRING>"))
+                    .withPartitionBy("left", "id")
+                    .withTableArgument("right", DataTypes.of("ROW<id INT, city 
STRING>"))
+                    .withPartitionBy("right", "id")
+                    .build()) {
+
+        // Use processElementForTable() to target specific tables
+        harness.processElementForTable("left", Row.of(1, "Alice"));
+        harness.processElementForTable("right", Row.of(1, "Berlin"));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output.get(0)).isEqualTo(Row.of(1, null, "LEFT: +I[1, 
Alice]"));
+        assertThat(output.get(1)).isEqualTo(Row.of(null, 1, "RIGHT: +I[1, 
Berlin]"));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing with Scalar Arguments
+
+Use `.withScalarArgument()` to configure scalar parameter values:
+
+{{< tabs "scalar-args" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<value INT>")
+public class FilterPTF extends ProcessTableFunction<Row> {
+    public void eval(
+            @ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row input,
+            @ArgumentHint(ArgumentTrait.SCALAR) int threshold) {
+        int value = input.getFieldAs("value");
+        if (value > threshold) {
+            collect(Row.of(value));
+        }
+    }
+}
+
+@Test
+void testFilter() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(FilterPTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .withScalarArgument("threshold", 50)  // Configure scalar 
value
+                    .build()) {
+
+        harness.processElement(Row.of(30));
+        harness.processElement(Row.of(70));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).containsExactly(Row.of(70));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+**Scalar-Only PTFs**: For PTFs with only scalar arguments, use `invoke()` to 
trigger evaluation:
+
+{{< tabs "scalar-only" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<sum INT>")
+public class AddPTF extends ProcessTableFunction<Row> {
+    public void eval(
+            @ArgumentHint(ArgumentTrait.SCALAR) int a,
+            @ArgumentHint(ArgumentTrait.SCALAR) int b) {
+        collect(Row.of(a + b));
+    }
+}
+
+@Test
+void testScalarOnly() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(AddPTF.class)
+                    .withScalarArgument("a", 5)
+                    .withScalarArgument("b", 7)
+                    .build()) {
+
+        harness.invoke();  // Use invoke() instead of processElement()
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).containsExactly(Row.of(12));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Configuring Table Argument Types
+
+The harness supports two ways to specify table argument types:
+
+{{< tabs "type-config" >}}
+{{< tab "Java" >}}
+```java
+// Option 1: Inline type annotation using DataTypeHint
+@DataTypeHint("ROW<doubled INT>")
+public class InlineTypePTF extends ProcessTableFunction<Row> {
+    public void eval(
+            @ArgumentHint(
+                value = ArgumentTrait.ROW_SEMANTIC_TABLE,
+                type = @DataTypeHint("ROW<value INT>")
+            ) Row input) {
+        int value = input.getFieldAs("value");
+        collect(Row.of(value * 2));
+    }
+}
+
+@Test
+void testInlineType() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            
ProcessTableFunctionTestHarness.ofClass(InlineTypePTF.class).build()) {
+
+        harness.processElement(Row.of(5));
+        assertThat(harness.getOutput()).containsExactly(Row.of(10));
+    }
+}
+
+// Option 2: Builder configuration
+@Test
+void testBuilderType() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(DoublePTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .build()) {
+
+        harness.processElement(Row.of(5));
+        assertThat(harness.getOutput()).containsExactly(Row.of(10));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+**Structured Types**: The harness supports structured POJO types in addition 
to `Row`, both as PTF inputs
+and outputs:
+
+{{< tabs "pojo-types" >}}
+{{< tab "Java" >}}
+```java
+public static class Customer {
+    public String name;
+    public int age;
+}
+
+@DataTypeHint("ROW<name STRING, age INT>")
+public class CustomerPTF extends ProcessTableFunction<Customer> {
+    public void eval(@ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Customer 
c) {
+        collect(c);
+    }
+}
+
+@Test
+void testPOJO() throws Exception {
+    try (ProcessTableFunctionTestHarness<Customer> harness =
+            ProcessTableFunctionTestHarness.ofClass(CustomerPTF.class)
+                    .withTableArgument("input", DataTypes.of(Customer.class))

Review Comment:
   Should not not be necessary if the type is explicit in the function?
   ```suggestion
                       .withTableArgument("input")
   ```



##########
docs/content/docs/dev/table/testing/ptf_test_harness.md:
##########
@@ -0,0 +1,361 @@
+---
+title: "Testing Process Table Functions"
+weight: 10
+type: docs
+---
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Testing Process Table Functions
+
+The `ProcessTableFunctionTestHarness` provides a lightweight unit testing 
framework for Process Table
+Functions (PTFs). It allows you to test PTF logic without starting a full 
Flink cluster. It is useful
+for testing and validating PTF business logic, multi-table PTF behaviour and 
validating errors.
+
+For end-to-end integration testing with the full Flink planner and runtime, 
use integration tests
+instead.
+
+{{< top >}}
+
+## Quick Start
+
+{{< tabs "quickstart" >}}
+{{< tab "Java" >}}
+```java
+import org.apache.flink.table.annotation.*;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.functions.ProcessTableFunction;
+import 
org.apache.flink.table.runtime.functions.ProcessTableFunctionTestHarness;
+import org.apache.flink.types.Row;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+// PTF under test
+@DataTypeHint("ROW<doubled INT>")
+public class DoublePTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row 
input) {
+        int value = input.getFieldAs("value");
+        collect(Row.of(value * 2));
+    }
+}
+
+// Test
+@Test
+void testDoublePTF() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(DoublePTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .build()) {
+
+        harness.processElement(Row.of(5));
+        harness.processElement(Row.of(10));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).hasSize(2);
+        assertThat(output.get(0)).isEqualTo(Row.of(10));
+        assertThat(output.get(1)).isEqualTo(Row.of(20));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+{{< top >}}
+
+## Common Testing Scenarios
+
+### Testing Row-Semantic Tables
+
+Use `.withTableArgument()` to configure the input table schema:
+
+{{< tabs "row-semantic" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<value INT>")
+public class PassthroughPTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row 
input) {
+        collect(input);
+    }
+}
+
+@Test
+void testPassthrough() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(PassthroughPTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .build()) {
+
+        harness.processElement(Row.of(42));
+        harness.processElement(Row.of(100));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).containsExactly(Row.of(42), Row.of(100));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing Set-Semantic Tables with Partitioning
+
+For `SET_SEMANTIC_TABLE`, use `.withPartitionBy()` to configure partition 
columns:
+
+{{< tabs "set-semantic" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<doubled INT>")
+public class PartitionedPTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row 
input) {
+        int value = input.getFieldAs("value");
+        collect(Row.of(value * 2));
+    }
+}
+
+@Test
+void testPartitionedPTF() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(PartitionedPTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<key STRING, 
value INT>"))
+                    .withPartitionBy("input", "key")
+                    .build()) {
+
+        harness.processElement(Row.of("A", 10));
+        harness.processElement(Row.of("B", 20));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output.get(0)).isEqualTo(Row.of("A", 20));
+        assertThat(output.get(1)).isEqualTo(Row.of("B", 40));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing Multiple Table Arguments
+
+Use `processElementForTable()` to specify which table receives each row:
+
+{{< tabs "multi-table" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<output STRING>")
+public class JoinPTF extends ProcessTableFunction<Row> {
+    public void eval(
+            @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row left,
+            @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row right) {
+        if (left != null) {
+            collect(Row.of("LEFT: " + left));
+        }
+        if (right != null) {
+            collect(Row.of("RIGHT: " + right));
+        }
+    }
+}
+
+@Test
+void testMultiTable() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(JoinPTF.class)
+                    .withTableArgument("left", DataTypes.of("ROW<id INT, name 
STRING>"))
+                    .withPartitionBy("left", "id")
+                    .withTableArgument("right", DataTypes.of("ROW<id INT, city 
STRING>"))
+                    .withPartitionBy("right", "id")
+                    .build()) {
+
+        // Use processElementForTable() to target specific tables
+        harness.processElementForTable("left", Row.of(1, "Alice"));
+        harness.processElementForTable("right", Row.of(1, "Berlin"));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output.get(0)).isEqualTo(Row.of(1, null, "LEFT: +I[1, 
Alice]"));
+        assertThat(output.get(1)).isEqualTo(Row.of(null, 1, "RIGHT: +I[1, 
Berlin]"));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing with Scalar Arguments
+
+Use `.withScalarArgument()` to configure scalar parameter values:
+
+{{< tabs "scalar-args" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<value INT>")
+public class FilterPTF extends ProcessTableFunction<Row> {
+    public void eval(
+            @ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row input,
+            @ArgumentHint(ArgumentTrait.SCALAR) int threshold) {
+        int value = input.getFieldAs("value");
+        if (value > threshold) {
+            collect(Row.of(value));
+        }
+    }
+}
+
+@Test
+void testFilter() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(FilterPTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .withScalarArgument("threshold", 50)  // Configure scalar 
value
+                    .build()) {
+
+        harness.processElement(Row.of(30));
+        harness.processElement(Row.of(70));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).containsExactly(Row.of(70));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+**Scalar-Only PTFs**: For PTFs with only scalar arguments, use `invoke()` to 
trigger evaluation:
+
+{{< tabs "scalar-only" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<sum INT>")
+public class AddPTF extends ProcessTableFunction<Row> {
+    public void eval(
+            @ArgumentHint(ArgumentTrait.SCALAR) int a,
+            @ArgumentHint(ArgumentTrait.SCALAR) int b) {
+        collect(Row.of(a + b));
+    }
+}
+
+@Test
+void testScalarOnly() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(AddPTF.class)
+                    .withScalarArgument("a", 5)
+                    .withScalarArgument("b", 7)
+                    .build()) {
+
+        harness.invoke();  // Use invoke() instead of processElement()

Review Comment:
   Maybe better naming?
   ```suggestion
           harness.process();
   ```
   What is the error behavior if you call this method but there are actually 
table args?



##########
docs/content/docs/dev/table/testing/ptf_test_harness.md:
##########
@@ -0,0 +1,361 @@
+---
+title: "Testing Process Table Functions"
+weight: 10
+type: docs
+---
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Testing Process Table Functions
+
+The `ProcessTableFunctionTestHarness` provides a lightweight unit testing 
framework for Process Table
+Functions (PTFs). It allows you to test PTF logic without starting a full 
Flink cluster. It is useful
+for testing and validating PTF business logic, multi-table PTF behaviour and 
validating errors.
+
+For end-to-end integration testing with the full Flink planner and runtime, 
use integration tests
+instead.
+
+{{< top >}}
+
+## Quick Start
+
+{{< tabs "quickstart" >}}
+{{< tab "Java" >}}
+```java
+import org.apache.flink.table.annotation.*;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.functions.ProcessTableFunction;
+import 
org.apache.flink.table.runtime.functions.ProcessTableFunctionTestHarness;
+import org.apache.flink.types.Row;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+// PTF under test
+@DataTypeHint("ROW<doubled INT>")
+public class DoublePTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row 
input) {
+        int value = input.getFieldAs("value");
+        collect(Row.of(value * 2));
+    }
+}
+
+// Test
+@Test
+void testDoublePTF() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(DoublePTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .build()) {
+
+        harness.processElement(Row.of(5));
+        harness.processElement(Row.of(10));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).hasSize(2);
+        assertThat(output.get(0)).isEqualTo(Row.of(10));
+        assertThat(output.get(1)).isEqualTo(Row.of(20));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+{{< top >}}
+
+## Common Testing Scenarios
+
+### Testing Row-Semantic Tables
+
+Use `.withTableArgument()` to configure the input table schema:
+
+{{< tabs "row-semantic" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<value INT>")
+public class PassthroughPTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row 
input) {
+        collect(input);
+    }
+}
+
+@Test
+void testPassthrough() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(PassthroughPTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .build()) {
+
+        harness.processElement(Row.of(42));
+        harness.processElement(Row.of(100));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).containsExactly(Row.of(42), Row.of(100));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing Set-Semantic Tables with Partitioning
+
+For `SET_SEMANTIC_TABLE`, use `.withPartitionBy()` to configure partition 
columns:
+
+{{< tabs "set-semantic" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<doubled INT>")
+public class PartitionedPTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row 
input) {
+        int value = input.getFieldAs("value");
+        collect(Row.of(value * 2));
+    }
+}
+
+@Test
+void testPartitionedPTF() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(PartitionedPTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<key STRING, 
value INT>"))
+                    .withPartitionBy("input", "key")
+                    .build()) {
+
+        harness.processElement(Row.of("A", 10));
+        harness.processElement(Row.of("B", 20));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output.get(0)).isEqualTo(Row.of("A", 20));
+        assertThat(output.get(1)).isEqualTo(Row.of("B", 40));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing Multiple Table Arguments
+
+Use `processElementForTable()` to specify which table receives each row:
+
+{{< tabs "multi-table" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<output STRING>")
+public class JoinPTF extends ProcessTableFunction<Row> {
+    public void eval(
+            @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row left,
+            @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row right) {
+        if (left != null) {
+            collect(Row.of("LEFT: " + left));
+        }
+        if (right != null) {
+            collect(Row.of("RIGHT: " + right));
+        }
+    }
+}
+
+@Test
+void testMultiTable() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(JoinPTF.class)
+                    .withTableArgument("left", DataTypes.of("ROW<id INT, name 
STRING>"))
+                    .withPartitionBy("left", "id")
+                    .withTableArgument("right", DataTypes.of("ROW<id INT, city 
STRING>"))
+                    .withPartitionBy("right", "id")
+                    .build()) {
+
+        // Use processElementForTable() to target specific tables
+        harness.processElementForTable("left", Row.of(1, "Alice"));
+        harness.processElementForTable("right", Row.of(1, "Berlin"));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output.get(0)).isEqualTo(Row.of(1, null, "LEFT: +I[1, 
Alice]"));
+        assertThat(output.get(1)).isEqualTo(Row.of(null, 1, "RIGHT: +I[1, 
Berlin]"));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing with Scalar Arguments
+
+Use `.withScalarArgument()` to configure scalar parameter values:
+
+{{< tabs "scalar-args" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<value INT>")
+public class FilterPTF extends ProcessTableFunction<Row> {
+    public void eval(
+            @ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row input,
+            @ArgumentHint(ArgumentTrait.SCALAR) int threshold) {
+        int value = input.getFieldAs("value");
+        if (value > threshold) {
+            collect(Row.of(value));
+        }
+    }
+}
+
+@Test
+void testFilter() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(FilterPTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .withScalarArgument("threshold", 50)  // Configure scalar 
value
+                    .build()) {
+
+        harness.processElement(Row.of(30));
+        harness.processElement(Row.of(70));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).containsExactly(Row.of(70));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+**Scalar-Only PTFs**: For PTFs with only scalar arguments, use `invoke()` to 
trigger evaluation:
+
+{{< tabs "scalar-only" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<sum INT>")
+public class AddPTF extends ProcessTableFunction<Row> {
+    public void eval(
+            @ArgumentHint(ArgumentTrait.SCALAR) int a,
+            @ArgumentHint(ArgumentTrait.SCALAR) int b) {
+        collect(Row.of(a + b));
+    }
+}
+
+@Test
+void testScalarOnly() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(AddPTF.class)
+                    .withScalarArgument("a", 5)
+                    .withScalarArgument("b", 7)
+                    .build()) {
+
+        harness.invoke();  // Use invoke() instead of processElement()
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).containsExactly(Row.of(12));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Configuring Table Argument Types
+
+The harness supports two ways to specify table argument types:
+
+{{< tabs "type-config" >}}
+{{< tab "Java" >}}
+```java
+// Option 1: Inline type annotation using DataTypeHint
+@DataTypeHint("ROW<doubled INT>")
+public class InlineTypePTF extends ProcessTableFunction<Row> {
+    public void eval(
+            @ArgumentHint(
+                value = ArgumentTrait.ROW_SEMANTIC_TABLE,
+                type = @DataTypeHint("ROW<value INT>")
+            ) Row input) {
+        int value = input.getFieldAs("value");
+        collect(Row.of(value * 2));
+    }
+}
+
+@Test
+void testInlineType() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            
ProcessTableFunctionTestHarness.ofClass(InlineTypePTF.class).build()) {
+
+        harness.processElement(Row.of(5));
+        assertThat(harness.getOutput()).containsExactly(Row.of(10));
+    }
+}
+
+// Option 2: Builder configuration
+@Test
+void testBuilderType() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(DoublePTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .build()) {
+
+        harness.processElement(Row.of(5));
+        assertThat(harness.getOutput()).containsExactly(Row.of(10));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+**Structured Types**: The harness supports structured POJO types in addition 
to `Row`, both as PTF inputs
+and outputs:
+
+{{< tabs "pojo-types" >}}
+{{< tab "Java" >}}
+```java
+public static class Customer {
+    public String name;
+    public int age;
+}
+
+@DataTypeHint("ROW<name STRING, age INT>")

Review Comment:
   
   ```suggestion
   // no annotation necessary, all types can be derived from class information
   ```



##########
docs/content/docs/dev/table/testing/ptf_test_harness.md:
##########
@@ -0,0 +1,361 @@
+---
+title: "Testing Process Table Functions"
+weight: 10
+type: docs
+---
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Testing Process Table Functions
+
+The `ProcessTableFunctionTestHarness` provides a lightweight unit testing 
framework for Process Table
+Functions (PTFs). It allows you to test PTF logic without starting a full 
Flink cluster. It is useful
+for testing and validating PTF business logic, multi-table PTF behaviour and 
validating errors.
+
+For end-to-end integration testing with the full Flink planner and runtime, 
use integration tests
+instead.
+
+{{< top >}}
+
+## Quick Start
+
+{{< tabs "quickstart" >}}
+{{< tab "Java" >}}
+```java
+import org.apache.flink.table.annotation.*;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.functions.ProcessTableFunction;
+import 
org.apache.flink.table.runtime.functions.ProcessTableFunctionTestHarness;
+import org.apache.flink.types.Row;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+// PTF under test
+@DataTypeHint("ROW<doubled INT>")
+public class DoublePTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row 
input) {
+        int value = input.getFieldAs("value");
+        collect(Row.of(value * 2));
+    }
+}
+
+// Test
+@Test
+void testDoublePTF() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(DoublePTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .build()) {
+
+        harness.processElement(Row.of(5));
+        harness.processElement(Row.of(10));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).hasSize(2);
+        assertThat(output.get(0)).isEqualTo(Row.of(10));
+        assertThat(output.get(1)).isEqualTo(Row.of(20));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+{{< top >}}
+
+## Common Testing Scenarios
+
+### Testing Row-Semantic Tables
+
+Use `.withTableArgument()` to configure the input table schema:
+
+{{< tabs "row-semantic" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<value INT>")
+public class PassthroughPTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row 
input) {
+        collect(input);
+    }
+}
+
+@Test
+void testPassthrough() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(PassthroughPTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<value INT>"))
+                    .build()) {
+
+        harness.processElement(Row.of(42));
+        harness.processElement(Row.of(100));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output).containsExactly(Row.of(42), Row.of(100));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing Set-Semantic Tables with Partitioning
+
+For `SET_SEMANTIC_TABLE`, use `.withPartitionBy()` to configure partition 
columns:
+
+{{< tabs "set-semantic" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<doubled INT>")
+public class PartitionedPTF extends ProcessTableFunction<Row> {
+    public void eval(@ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row 
input) {
+        int value = input.getFieldAs("value");
+        collect(Row.of(value * 2));
+    }
+}
+
+@Test
+void testPartitionedPTF() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(PartitionedPTF.class)
+                    .withTableArgument("input", DataTypes.of("ROW<key STRING, 
value INT>"))
+                    .withPartitionBy("input", "key")
+                    .build()) {
+
+        harness.processElement(Row.of("A", 10));
+        harness.processElement(Row.of("B", 20));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output.get(0)).isEqualTo(Row.of("A", 20));
+        assertThat(output.get(1)).isEqualTo(Row.of("B", 40));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing Multiple Table Arguments
+
+Use `processElementForTable()` to specify which table receives each row:
+
+{{< tabs "multi-table" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<output STRING>")
+public class JoinPTF extends ProcessTableFunction<Row> {
+    public void eval(
+            @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row left,
+            @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row right) {
+        if (left != null) {
+            collect(Row.of("LEFT: " + left));
+        }
+        if (right != null) {
+            collect(Row.of("RIGHT: " + right));
+        }
+    }
+}
+
+@Test
+void testMultiTable() throws Exception {
+    try (ProcessTableFunctionTestHarness<Row> harness =
+            ProcessTableFunctionTestHarness.ofClass(JoinPTF.class)
+                    .withTableArgument("left", DataTypes.of("ROW<id INT, name 
STRING>"))
+                    .withPartitionBy("left", "id")
+                    .withTableArgument("right", DataTypes.of("ROW<id INT, city 
STRING>"))
+                    .withPartitionBy("right", "id")
+                    .build()) {
+
+        // Use processElementForTable() to target specific tables
+        harness.processElementForTable("left", Row.of(1, "Alice"));
+        harness.processElementForTable("right", Row.of(1, "Berlin"));
+
+        List<Row> output = harness.getOutput();
+        assertThat(output.get(0)).isEqualTo(Row.of(1, null, "LEFT: +I[1, 
Alice]"));
+        assertThat(output.get(1)).isEqualTo(Row.of(null, 1, "RIGHT: +I[1, 
Berlin]"));
+    }
+}
+```
+{{< /tab >}}
+{{< /tabs >}}
+
+### Testing with Scalar Arguments
+
+Use `.withScalarArgument()` to configure scalar parameter values:
+
+{{< tabs "scalar-args" >}}
+{{< tab "Java" >}}
+```java
+@DataTypeHint("ROW<value INT>")
+public class FilterPTF extends ProcessTableFunction<Row> {
+    public void eval(
+            @ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row input,
+            @ArgumentHint(ArgumentTrait.SCALAR) int threshold) {

Review Comment:
   For simplification, this is not necessary
   ```suggestion
               int threshold) {
   ```



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

To unsubscribe, e-mail: [email protected]

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

Reply via email to