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

snuyanzin 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 ba41c1f950b [FLINK-40039][table] Fix `CTAS`/`RTAS` when used with 
set-semantic PTFs
ba41c1f950b is described below

commit ba41c1f950bab2663cf0c323076bce7347d2fef8
Author: Ramin Gharib <[email protected]>
AuthorDate: Tue Jul 7 22:46:14 2026 +0200

    [FLINK-40039][table] Fix `CTAS`/`RTAS` when used with set-semantic PTFs
---
 .../MaterializedTableManager.java                  |   6 +-
 .../AlterMaterializedTableAsQueryOperation.java    |   4 +-
 .../AlterMaterializedTableChangeOperation.java     |  14 +--
 .../CreateMaterializedTableOperation.java          |  12 +--
 .../operations/converters/MergeTableAsUtil.java    | 106 +++++++++------------
 .../AbstractCreateMaterializedTableConverter.java  |  20 ++--
 ...SqlCreateOrAlterMaterializedTableConverter.java |  52 ++++------
 .../table/SqlCreateTableAsConverter.java           |  14 +--
 .../table/SqlReplaceTableAsConverter.java          |   6 +-
 .../table/planner/delegation/PlannerBase.scala     |   2 +-
 ...erializedTableNodeToOperationConverterTest.java |  16 ++++
 .../planner/runtime/stream/sql/CTASITCase.java     |  65 +++++++++++++
 .../planner/runtime/stream/sql/RTASITCase.java     |  23 +++++
 ...ExplainCtasWithColumnsInCreateAndQueryParts.out |  12 +--
 .../planner/plan/stream/sql/TableSinkTest.scala    |  60 +++++++-----
 15 files changed, 249 insertions(+), 163 deletions(-)

diff --git 
a/flink-table/flink-sql-gateway/src/main/java/org/apache/flink/table/gateway/service/materializedtable/MaterializedTableManager.java
 
b/flink-table/flink-sql-gateway/src/main/java/org/apache/flink/table/gateway/service/materializedtable/MaterializedTableManager.java
index 56cf110880a..cc01f96e7ec 100644
--- 
a/flink-table/flink-sql-gateway/src/main/java/org/apache/flink/table/gateway/service/materializedtable/MaterializedTableManager.java
+++ 
b/flink-table/flink-sql-gateway/src/main/java/org/apache/flink/table/gateway/service/materializedtable/MaterializedTableManager.java
@@ -955,7 +955,7 @@ public class MaterializedTableManager {
                             op.getTableIdentifier(),
                             oldTable -> op.getTableChanges(),
                             suspendMaterializedTable,
-                            op.getSinkModifyQuery());
+                            op.getAsQueryOperation());
             operationExecutor.callExecutableOperation(
                     handle, alterMaterializedTableChangeOperation);
 
@@ -1014,7 +1014,7 @@ public class MaterializedTableManager {
                             tableIdentifier,
                             oldTable -> tableChanges,
                             oldMaterializedTable,
-                            op.getSinkModifyQuery());
+                            op.getAsQueryOperation());
 
             operationExecutor.callExecutableOperation(
                     handle, alterMaterializedTableChangeOperation);
@@ -1036,7 +1036,7 @@ public class MaterializedTableManager {
                 op.getTableIdentifier(),
                 oldTable -> List.of(),
                 oldMaterializedTable,
-                op.getSinkModifyQuery());
+                op.getAsQueryOperation());
     }
 
     private TableChange.ModifyRefreshHandler generateResetSavepointTableChange(
diff --git 
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/AlterMaterializedTableAsQueryOperation.java
 
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/AlterMaterializedTableAsQueryOperation.java
index 9da8302ca60..0dcbfd72926 100644
--- 
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/AlterMaterializedTableAsQueryOperation.java
+++ 
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/AlterMaterializedTableAsQueryOperation.java
@@ -42,8 +42,8 @@ public class AlterMaterializedTableAsQueryOperation extends 
AlterMaterializedTab
             ObjectIdentifier tableIdentifier,
             Function<ResolvedCatalogMaterializedTable, List<TableChange>> 
tableChangesForTable,
             ResolvedCatalogMaterializedTable oldTable,
-            QueryOperation sinkModifyQuery) {
-        super(tableIdentifier, tableChangesForTable, oldTable, 
sinkModifyQuery);
+            QueryOperation asQueryOperation) {
+        super(tableIdentifier, tableChangesForTable, oldTable, 
asQueryOperation);
     }
 
     @Override
diff --git 
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/AlterMaterializedTableChangeOperation.java
 
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/AlterMaterializedTableChangeOperation.java
index 87d588a920d..299c72d6cfe 100644
--- 
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/AlterMaterializedTableChangeOperation.java
+++ 
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/AlterMaterializedTableChangeOperation.java
@@ -57,7 +57,7 @@ public class AlterMaterializedTableChangeOperation extends 
AlterMaterializedTabl
         implements ModifyOperation {
 
     private final Function<ResolvedCatalogMaterializedTable, 
List<TableChange>> tableChangeForTable;
-    private final QueryOperation sinkModifyQuery;
+    private final QueryOperation asQueryOperation;
     private ResolvedCatalogMaterializedTable oldTable;
     private MaterializedTableChangeHandler handler;
     private CatalogMaterializedTable newTable;
@@ -75,15 +75,15 @@ public class AlterMaterializedTableChangeOperation extends 
AlterMaterializedTabl
             ObjectIdentifier tableIdentifier,
             Function<ResolvedCatalogMaterializedTable, List<TableChange>> 
tableChangeForTable,
             ResolvedCatalogMaterializedTable oldTable,
-            QueryOperation sinkModifyQuery) {
+            QueryOperation asQueryOperation) {
         super(tableIdentifier);
         this.tableChangeForTable = tableChangeForTable;
         this.oldTable = oldTable;
-        this.sinkModifyQuery = sinkModifyQuery;
+        this.asQueryOperation = asQueryOperation;
     }
 
-    public QueryOperation getSinkModifyQuery() {
-        return sinkModifyQuery;
+    public QueryOperation getAsQueryOperation() {
+        return asQueryOperation;
     }
 
     public List<TableChange> getTableChanges() {
@@ -95,7 +95,7 @@ public class AlterMaterializedTableChangeOperation extends 
AlterMaterializedTabl
 
     public AlterMaterializedTableChangeOperation copyAsTableChangeOperation() {
         return new AlterMaterializedTableChangeOperation(
-                tableIdentifier, tableChangeForTable, oldTable, 
sinkModifyQuery);
+                tableIdentifier, tableChangeForTable, oldTable, 
asQueryOperation);
     }
 
     public CatalogMaterializedTable getNewTable() {
@@ -165,7 +165,7 @@ public class AlterMaterializedTableChangeOperation extends 
AlterMaterializedTabl
 
     @Override
     public QueryOperation getChild() {
-        return this.sinkModifyQuery;
+        return this.asQueryOperation;
     }
 
     @Override
diff --git 
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/CreateMaterializedTableOperation.java
 
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/CreateMaterializedTableOperation.java
index ff425fe53aa..070e0f4948e 100644
--- 
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/CreateMaterializedTableOperation.java
+++ 
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/CreateMaterializedTableOperation.java
@@ -41,15 +41,15 @@ public class CreateMaterializedTableOperation
 
     private final ObjectIdentifier tableIdentifier;
     private final ResolvedCatalogMaterializedTable materializedTable;
-    private final QueryOperation sinkModifyingQuery;
+    private final QueryOperation asQueryOperation;
 
     public CreateMaterializedTableOperation(
             ObjectIdentifier tableIdentifier,
             ResolvedCatalogMaterializedTable materializedTable,
-            QueryOperation sinkModifyQuery) {
+            QueryOperation asQueryOperation) {
         this.tableIdentifier = tableIdentifier;
         this.materializedTable = materializedTable;
-        this.sinkModifyingQuery = sinkModifyQuery;
+        this.asQueryOperation = asQueryOperation;
     }
 
     @Override
@@ -67,8 +67,8 @@ public class CreateMaterializedTableOperation
         return materializedTable;
     }
 
-    public QueryOperation getSinkModifyingQuery() {
-        return sinkModifyingQuery;
+    public QueryOperation getAsQueryOperation() {
+        return asQueryOperation;
     }
 
     @Override
@@ -83,7 +83,7 @@ public class CreateMaterializedTableOperation
 
     @Override
     public QueryOperation getChild() {
-        return this.sinkModifyingQuery;
+        return this.asQueryOperation;
     }
 
     @Override
diff --git 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/MergeTableAsUtil.java
 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/MergeTableAsUtil.java
index 2b26585d6f4..e41e0e9ed08 100644
--- 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/MergeTableAsUtil.java
+++ 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/MergeTableAsUtil.java
@@ -28,30 +28,28 @@ import 
org.apache.flink.sql.parser.ddl.position.SqlTableColumnPosition;
 import org.apache.flink.table.api.Schema;
 import org.apache.flink.table.api.Schema.UnresolvedColumn;
 import org.apache.flink.table.api.Schema.UnresolvedPhysicalColumn;
-import org.apache.flink.table.api.TableException;
 import org.apache.flink.table.api.ValidationException;
-import org.apache.flink.table.catalog.CatalogManager;
 import org.apache.flink.table.catalog.DataTypeFactory;
 import org.apache.flink.table.catalog.ResolvedCatalogBaseTable;
 import org.apache.flink.table.catalog.ResolvedSchema;
 import org.apache.flink.table.planner.calcite.FlinkCalciteSqlValidator;
-import org.apache.flink.table.planner.calcite.FlinkPlannerImpl;
 import org.apache.flink.table.planner.calcite.FlinkTypeFactory;
-import org.apache.flink.table.planner.calcite.SqlRewriterUtils;
 import org.apache.flink.table.planner.operations.PlannerQueryOperation;
-import org.apache.flink.table.planner.operations.SqlNodeToOperationConversion;
 import 
org.apache.flink.table.planner.operations.converters.SqlNodeConverter.ConvertContext;
 import org.apache.flink.table.types.logical.LogicalType;
 import org.apache.flink.table.types.logical.RowType;
 
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.sql.SqlIdentifier;
-import org.apache.calcite.sql.SqlLiteral;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
-import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.validate.SqlValidator;
+import org.apache.calcite.tools.RelBuilder;
 
 import javax.annotation.Nullable;
 
@@ -88,80 +86,70 @@ public class MergeTableAsUtil {
     }
 
     /**
-     * Rewrites the query operation to include only the fields that may be 
persisted in the sink.
+     * Reshapes the query so its output columns line up with the sink's 
persistable columns:
+     * reordering them and filling sink columns the query does not produce 
with {@code NULL}.
+     * Returns the query unchanged when it already matches the sink 1:1. A 
sink column the query
+     * does not produce that is declared {@code NOT NULL} raises a {@link 
ValidationException}.
      */
     public PlannerQueryOperation maybeRewriteQuery(
-            CatalogManager catalogManager,
-            FlinkPlannerImpl flinkPlanner,
             PlannerQueryOperation origQueryOperation,
             SqlNode origQueryNode,
             ResolvedCatalogBaseTable<?> sinkTable) {
-        FlinkCalciteSqlValidator sqlValidator = 
flinkPlanner.getOrCreateSqlValidator();
-        SqlRewriterUtils rewriterUtils = new SqlRewriterUtils(sqlValidator);
-        FlinkTypeFactory typeFactory = (FlinkTypeFactory) 
sqlValidator.getTypeFactory();
-
-        // Only fields that may be persisted will be included in the select 
query
-        RowType sinkRowType =
-                ((RowType) 
sinkTable.getResolvedSchema().toSinkRowDataType().getLogicalType());
-
-        Map<String, Integer> sourceFields =
-                IntStream.range(0, 
origQueryOperation.getResolvedSchema().getColumnNames().size())
+        final RelNode queryRelNode = origQueryOperation.getCalciteTree();
+        final RelOptCluster cluster = queryRelNode.getCluster();
+        final RexBuilder rexBuilder = cluster.getRexBuilder();
+        final FlinkTypeFactory typeFactory = (FlinkTypeFactory) 
cluster.getTypeFactory();
+
+        // Only fields that may be persisted are included in the sink.
+        final RowType sinkRowType =
+                (RowType) 
sinkTable.getResolvedSchema().toSinkRowDataType().getLogicalType();
+
+        final List<String> sourceColumns = 
origQueryOperation.getResolvedSchema().getColumnNames();
+        final Map<String, Integer> sourceFields =
+                IntStream.range(0, sourceColumns.size())
                         .boxed()
-                        .collect(
-                                Collectors.toMap(
-                                        
origQueryOperation.getResolvedSchema().getColumnNames()
-                                                ::get,
-                                        Function.identity()));
+                        .collect(Collectors.toMap(sourceColumns::get, 
Function.identity()));
 
-        // assignedFields contains the new sink fields that are not present in 
the source
-        // and that will be included in the select query
-        LinkedHashMap<Integer, SqlNode> assignedFields = new LinkedHashMap<>();
-
-        // targetPositions contains the positions of the source fields that 
will be
-        // included in the select query
-        List<Object> targetPositions = new ArrayList<>();
+        final List<RexNode> projects = new ArrayList<>();
+        final List<String> fieldNames = new ArrayList<>();
+        // The projection is a no-op when the query already produces the sink 
columns 1:1 in order.
+        boolean rewriteNeeded = sinkRowType.getFieldCount() != 
sourceColumns.size();
 
+        // The loop cannot stop once a rewrite is detected: the projection 
must cover every sink
+        // field, and every missing NOT NULL column must still be validated.
         int pos = -1;
         for (RowType.RowField targetField : sinkRowType.getFields()) {
             pos++;
+            fieldNames.add(targetField.getName());
 
-            if (!sourceFields.containsKey(targetField.getName())) {
+            final Integer sourcePos = sourceFields.get(targetField.getName());
+            if (sourcePos == null) {
                 if (!targetField.getType().isNullable()) {
                     throw new ValidationException(
                             "Column '"
                                     + targetField.getName()
                                     + "' has no default value and does not 
allow NULLs.");
                 }
-
-                assignedFields.put(
-                        pos,
-                        validator.maybeCast(
-                                SqlLiteral.createNull(SqlParserPos.ZERO),
-                                typeFactory.createUnknownType(),
+                projects.add(
+                        rexBuilder.makeNullLiteral(
                                 
typeFactory.createFieldTypeFromLogicalType(targetField.getType())));
+                rewriteNeeded = true;
             } else {
-                targetPositions.add(sourceFields.get(targetField.getName()));
+                projects.add(rexBuilder.makeInputRef(queryRelNode, sourcePos));
+                if (sourcePos != pos) {
+                    rewriteNeeded = true;
+                }
             }
         }
 
-        // rewrite query
-        SqlCall newSelect =
-                rewriterUtils.rewriteCall(
-                        rewriterUtils,
-                        sqlValidator,
-                        (SqlCall) origQueryNode,
-                        typeFactory.buildRelNodeRowType(sinkRowType),
-                        assignedFields,
-                        targetPositions,
-                        () -> "Unsupported node type " + 
origQueryNode.getKind());
-
-        return (PlannerQueryOperation)
-                SqlNodeToOperationConversion.convert(flinkPlanner, 
catalogManager, newSelect)
-                        .orElseThrow(
-                                () ->
-                                        new TableException(
-                                                "Unsupported node type "
-                                                        + 
newSelect.getClass().getSimpleName()));
+        if (!rewriteNeeded) {
+            return origQueryOperation;
+        }
+
+        final RelBuilder relBuilder = 
RelFactories.LOGICAL_BUILDER.create(cluster, null);
+        final RelNode projected =
+                relBuilder.push(queryRelNode).project(projects, fieldNames, 
true).build();
+        return new PlannerQueryOperation(projected, () -> 
escapeExpression.apply(origQueryNode));
     }
 
     /**
diff --git 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/materializedtable/AbstractCreateMaterializedTableConverter.java
 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/materializedtable/AbstractCreateMaterializedTableConverter.java
index 3ed73870835..483f062b180 100644
--- 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/materializedtable/AbstractCreateMaterializedTableConverter.java
+++ 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/materializedtable/AbstractCreateMaterializedTableConverter.java
@@ -80,6 +80,8 @@ public abstract class 
AbstractCreateMaterializedTableConverter<T extends SqlCrea
 
         ResolvedSchema getMergedQuerySchema();
 
+        PlannerQueryOperation getAsQueryOperation();
+
         RefreshMode getMergedRefreshMode();
 
         LogicalRefreshMode getMergedLogicalRefreshMode();
@@ -121,16 +123,13 @@ public abstract class 
AbstractCreateMaterializedTableConverter<T extends SqlCrea
                         
.get(MaterializedTableConfigOptions.MATERIALIZED_TABLE_DEFAULT_START_MODE));
     }
 
-    protected final ResolvedSchema getQueryResolvedSchema(
+    protected final PlannerQueryOperation getAsQueryOperation(
             T sqlCreateMaterializedTable, ConvertContext context) {
-        SqlNode selectQuery = sqlCreateMaterializedTable.getAsQuery();
-        SqlNode validateQuery = 
context.getSqlValidator().validate(selectQuery);
-
-        PlannerQueryOperation queryOperation =
-                new PlannerQueryOperation(
-                        context.toRelRoot(validateQuery).project(),
-                        () -> context.toQuotedSqlString(validateQuery));
-        return queryOperation.getResolvedSchema();
+        final SqlNode selectQuery = sqlCreateMaterializedTable.getAsQuery();
+        final SqlNode validateQuery = 
context.getSqlValidator().validate(selectQuery);
+        return new PlannerQueryOperation(
+                context.toRelRoot(validateQuery).project(),
+                () -> context.toQuotedSqlString(validateQuery));
     }
 
     protected final LogicalRefreshMode getDerivedLogicalRefreshMode(T 
sqlCreateMaterializedTable) {
@@ -167,8 +166,7 @@ public abstract class 
AbstractCreateMaterializedTableConverter<T extends SqlCrea
     }
 
     protected final ResolvedCatalogMaterializedTable 
getResolvedCatalogMaterializedTable(
-            T sqlCreateMaterializedTable, ConvertContext context) {
-        final MergeContext mergeContext = 
getMergeContext(sqlCreateMaterializedTable, context);
+            MergeContext mergeContext, T sqlCreateMaterializedTable, 
ConvertContext context) {
         final List<String> partitionKeys = 
mergeContext.getMergedPartitionKeys();
         final Schema schema = mergeContext.getMergedSchema();
         final ResolvedSchema querySchema = mergeContext.getMergedQuerySchema();
diff --git 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/materializedtable/SqlCreateOrAlterMaterializedTableConverter.java
 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/materializedtable/SqlCreateOrAlterMaterializedTableConverter.java
index 286071d3f98..bb757cd91a4 100644
--- 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/materializedtable/SqlCreateOrAlterMaterializedTableConverter.java
+++ 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/materializedtable/SqlCreateOrAlterMaterializedTableConverter.java
@@ -45,7 +45,6 @@ import 
org.apache.flink.table.operations.materializedtable.ConvertTableToMateria
 import 
org.apache.flink.table.operations.materializedtable.CreateMaterializedTableOperation;
 import 
org.apache.flink.table.operations.materializedtable.FullAlterMaterializedTableOperation;
 import 
org.apache.flink.table.operations.materializedtable.MaterializedTableChangeHandler;
-import org.apache.flink.table.planner.calcite.FlinkPlannerImpl;
 import org.apache.flink.table.planner.operations.PlannerQueryOperation;
 import org.apache.flink.table.planner.operations.converters.MergeTableAsUtil;
 import org.apache.flink.table.planner.utils.MaterializedTableUtils;
@@ -61,8 +60,6 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 
-import static 
org.apache.flink.table.planner.operations.converters.SqlNodeConvertUtils.toQueryOperation;
-
 /** A converter for {@link SqlCreateOrAlterMaterializedTable}. */
 public class SqlCreateOrAlterMaterializedTableConverter
         extends 
AbstractCreateMaterializedTableConverter<SqlCreateOrAlterMaterializedTable> {
@@ -125,10 +122,7 @@ public class SqlCreateOrAlterMaterializedTableConverter
             final ObjectIdentifier identifier) {
         final SchemaResolver schemaResolver = 
context.getCatalogManager().getSchemaResolver();
         final MergeContext mergeContext = 
getMergeContext(sqlCreateOrAlterTable, context);
-        final SqlNode asQuerySqlNode = sqlCreateOrAlterTable.getAsQuery();
-        final FlinkPlannerImpl flinkPlanner = context.getFlinkPlanner();
-        final SqlNode validatedAsQuery = flinkPlanner.validate(asQuerySqlNode);
-        final PlannerQueryOperation asQuery = 
toQueryOperation(validatedAsQuery, context);
+        final PlannerQueryOperation asQuery = 
mergeContext.getAsQueryOperation();
 
         return new FullAlterMaterializedTableOperation(
                 identifier,
@@ -172,17 +166,11 @@ public class SqlCreateOrAlterMaterializedTableConverter
         final ResolvedCatalogMaterializedTable resolvedNewMaterializedTable =
                 
context.getCatalogManager().resolveCatalogMaterializedTable(newMaterializedTable);
 
-        final SqlNode asQuerySqlNode = 
sqlCreateOrAlterMaterializedTable.getAsQuery();
-        final FlinkPlannerImpl flinkPlanner = context.getFlinkPlanner();
-        final SqlNode validatedAsQuery = flinkPlanner.validate(asQuerySqlNode);
-        final PlannerQueryOperation asQuery = 
toQueryOperation(validatedAsQuery, context);
-        final PlannerQueryOperation sinkQuery =
+        final PlannerQueryOperation asQueryOperation =
                 new MergeTableAsUtil(context)
                         .maybeRewriteQuery(
-                                context.getCatalogManager(),
-                                flinkPlanner,
-                                asQuery,
-                                validatedAsQuery,
+                                baseMergeContext.getAsQueryOperation(),
+                                sqlCreateOrAlterMaterializedTable.getAsQuery(),
                                 resolvedNewMaterializedTable);
 
         return new ConvertTableToMaterializedTableOperation(
@@ -195,7 +183,7 @@ public class SqlCreateOrAlterMaterializedTableConverter
                                 resolvedCatalogMaterializedTable,
                                 baseMergeContext.hasSchemaDefinition(),
                                 baseMergeContext.hasConstraintDefinition()),
-                sinkQuery);
+                asQueryOperation);
     }
 
     private List<TableChange> buildConversionTableChanges(
@@ -259,22 +247,17 @@ public class SqlCreateOrAlterMaterializedTableConverter
             final SqlCreateOrAlterMaterializedTable sqlCreateOrAlterTable,
             final ConvertContext context,
             final ObjectIdentifier identifier) {
+        final MergeContext mergeContext = 
getMergeContext(sqlCreateOrAlterTable, context);
         final ResolvedCatalogMaterializedTable resolvedTable =
-                getResolvedCatalogMaterializedTable(sqlCreateOrAlterTable, 
context);
-        final SqlNode asQuerySqlNode = sqlCreateOrAlterTable.getAsQuery();
-        final FlinkPlannerImpl flinkPlanner = context.getFlinkPlanner();
-        final SqlNode validatedAsQuery = flinkPlanner.validate(asQuerySqlNode);
-        final PlannerQueryOperation asQuery = 
toQueryOperation(validatedAsQuery, context);
-        final PlannerQueryOperation sinkQuery =
+                getResolvedCatalogMaterializedTable(mergeContext, 
sqlCreateOrAlterTable, context);
+        final PlannerQueryOperation asQueryOperation =
                 new MergeTableAsUtil(context)
                         .maybeRewriteQuery(
-                                context.getCatalogManager(),
-                                flinkPlanner,
-                                asQuery,
-                                validatedAsQuery,
+                                mergeContext.getAsQueryOperation(),
+                                sqlCreateOrAlterTable.getAsQuery(),
                                 resolvedTable);
 
-        return new CreateMaterializedTableOperation(identifier, resolvedTable, 
sinkQuery);
+        return new CreateMaterializedTableOperation(identifier, resolvedTable, 
asQueryOperation);
     }
 
     private List<TableChange> buildTableChanges(
@@ -438,10 +421,12 @@ public class SqlCreateOrAlterMaterializedTableConverter
                     
SqlCreateOrAlterMaterializedTableConverter.this.getDerivedOriginalQuery(
                             sqlCreateMaterializedTable, context);
 
-            private final ResolvedSchema querySchema =
-                    
SqlCreateOrAlterMaterializedTableConverter.this.getQueryResolvedSchema(
+            private final PlannerQueryOperation asQueryOperation =
+                    
SqlCreateOrAlterMaterializedTableConverter.this.getAsQueryOperation(
                             sqlCreateMaterializedTable, context);
 
+            private final ResolvedSchema querySchema = 
asQueryOperation.getResolvedSchema();
+
             @Override
             public boolean hasSchemaDefinition() {
                 final SqlNodeList sqlNodeList = 
sqlCreateMaterializedTable.getColumnList();
@@ -506,7 +491,12 @@ public class SqlCreateOrAlterMaterializedTableConverter
 
             @Override
             public ResolvedSchema getMergedQuerySchema() {
-                return this.querySchema;
+                return asQueryOperation.getResolvedSchema();
+            }
+
+            @Override
+            public PlannerQueryOperation getAsQueryOperation() {
+                return asQueryOperation;
             }
 
             @Override
diff --git 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/table/SqlCreateTableAsConverter.java
 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/table/SqlCreateTableAsConverter.java
index 8fa2702a329..b0761ebbfdd 100644
--- 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/table/SqlCreateTableAsConverter.java
+++ 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/table/SqlCreateTableAsConverter.java
@@ -47,18 +47,17 @@ public class SqlCreateTableAsConverter extends 
AbstractCreateTableConverter<SqlC
     public Operation convertSqlNode(SqlCreateTableAs sqlCreateTableAs, 
ConvertContext context) {
         final FlinkPlannerImpl flinkPlanner = context.getFlinkPlanner();
         final CatalogManager catalogManager = context.getCatalogManager();
-        SqlNode asQuerySqlNode = sqlCreateTableAs.getAsQuery();
-        SqlNode validatedAsQuery = flinkPlanner.validate(asQuerySqlNode);
+        final SqlNode asQuerySqlNode = sqlCreateTableAs.getAsQuery();
 
         PlannerQueryOperation query =
                 (PlannerQueryOperation)
                         SqlNodeToOperationConversion.convert(
-                                        flinkPlanner, catalogManager, 
validatedAsQuery)
+                                        flinkPlanner, catalogManager, 
asQuerySqlNode)
                                 .orElseThrow(
                                         () ->
                                                 new TableException(
                                                         "CTAS unsupported node 
type "
-                                                                + 
validatedAsQuery
+                                                                + 
asQuerySqlNode
                                                                         
.getClass()
                                                                         
.getSimpleName()));
         ResolvedCatalogTable tableWithResolvedSchema =
@@ -67,12 +66,7 @@ public class SqlCreateTableAsConverter extends 
AbstractCreateTableConverter<SqlC
         // If needed, rewrite the query to include the new sink fields in the 
select list
         query =
                 new MergeTableAsUtil(context)
-                        .maybeRewriteQuery(
-                                catalogManager,
-                                flinkPlanner,
-                                query,
-                                validatedAsQuery,
-                                tableWithResolvedSchema);
+                        .maybeRewriteQuery(query, asQuerySqlNode, 
tableWithResolvedSchema);
 
         ObjectIdentifier identifier = getIdentifier(sqlCreateTableAs, context);
         CreateTableOperation createTableOperation =
diff --git 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/table/SqlReplaceTableAsConverter.java
 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/table/SqlReplaceTableAsConverter.java
index 438575d8f94..34b9c10a84e 100644
--- 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/table/SqlReplaceTableAsConverter.java
+++ 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/table/SqlReplaceTableAsConverter.java
@@ -69,11 +69,7 @@ public class SqlReplaceTableAsConverter extends 
AbstractCreateTableConverter<Sql
         query =
                 new MergeTableAsUtil(context)
                         .maybeRewriteQuery(
-                                context.getCatalogManager(),
-                                flinkPlanner,
-                                query,
-                                sqlReplaceTableAs.getAsQuery(),
-                                tableWithResolvedSchema);
+                                query, sqlReplaceTableAs.getAsQuery(), 
tableWithResolvedSchema);
 
         ObjectIdentifier identifier = getIdentifier(sqlReplaceTableAs, 
context);
         CreateTableOperation createTableOperation =
diff --git 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/delegation/PlannerBase.scala
 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/delegation/PlannerBase.scala
index 65c36d73151..e4f76fd1990 100644
--- 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/delegation/PlannerBase.scala
+++ 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/delegation/PlannerBase.scala
@@ -298,7 +298,7 @@ abstract class PlannerBase(
         )
 
       case alterMtOperation: AlterMaterializedTableChangeOperation
-          if alterMtOperation.getSinkModifyQuery != null =>
+          if alterMtOperation.getAsQueryOperation != null =>
         val newTable =
           
catalogManager.resolveCatalogMaterializedTable(alterMtOperation.getNewTable)
         // The alter reuses the existing storage, so keep the old table's 
resolved connector
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/operations/SqlMaterializedTableNodeToOperationConverterTest.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/operations/SqlMaterializedTableNodeToOperationConverterTest.java
index 1d2cd769da8..3c4aeac8343 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/operations/SqlMaterializedTableNodeToOperationConverterTest.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/operations/SqlMaterializedTableNodeToOperationConverterTest.java
@@ -53,6 +53,7 @@ import 
org.apache.flink.table.operations.materializedtable.AlterMaterializedTabl
 import 
org.apache.flink.table.operations.materializedtable.CreateMaterializedTableOperation;
 import 
org.apache.flink.table.operations.materializedtable.DropMaterializedTableOperation;
 import 
org.apache.flink.table.operations.materializedtable.FullAlterMaterializedTableOperation;
+import 
org.apache.flink.table.planner.plan.nodes.exec.stream.ProcessTableFunctionTestUtils.SetSemanticTableFunction;
 import org.apache.flink.table.planner.utils.TableFunc0;
 
 import org.junit.jupiter.api.BeforeEach;
@@ -197,6 +198,21 @@ class SqlMaterializedTableNodeToOperationConverterTest
                 sqlWithNonPersistedLast, "base_mtbl_with_non_persisted_last");
     }
 
+    @Test
+    void testCreateMaterializedTableAsSelectWithSetSemanticTablePtf() {
+        functionCatalog.registerTemporarySystemFunction("f", new 
SetSemanticTableFunction(), false);
+        final String sql =
+                "CREATE MATERIALIZED TABLE mt_ptf\n"
+                        + "WITH (\n"
+                        + "  'connector' = 'filesystem',\n"
+                        + "  'format' = 'json'\n"
+                        + ")\n"
+                        + "AS SELECT * FROM f(r => TABLE t1 PARTITION BY a, i 
=> 1)";
+
+        final Operation operation = parse(sql);
+        
assertThat(operation).isInstanceOf(CreateMaterializedTableOperation.class);
+    }
+
     @Test
     void testCreateMaterializedTable() {
         final String sql =
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/CTASITCase.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/CTASITCase.java
new file mode 100644
index 00000000000..e6f52b02649
--- /dev/null
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/CTASITCase.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.runtime.stream.sql;
+
+import org.apache.flink.table.catalog.ResolvedSchema;
+import org.apache.flink.table.functions.ProcessTableFunction;
+import org.apache.flink.table.planner.factories.TestValuesTableFactory;
+import 
org.apache.flink.table.planner.plan.nodes.exec.stream.ProcessTableFunctionTestUtils.SetSemanticTableFunction;
+import org.apache.flink.table.planner.runtime.utils.StreamingTestBase;
+import org.apache.flink.table.planner.runtime.utils.TestData;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** IT Case for CREATE TABLE AS SELECT statement over a {@link 
ProcessTableFunction}. */
+class CTASITCase extends StreamingTestBase {
+
+    @BeforeEach
+    @Override
+    public void before() throws Exception {
+        super.before();
+        final String dataId = 
TestValuesTableFactory.registerData(TestData.smallData3());
+        tEnv().executeSql(
+                        String.format(
+                                "CREATE TABLE source(a int, b bigint, c 
string)"
+                                        + " WITH ('connector' = 'values', 
'bounded' = 'true', 'data-id' = '%s')",
+                                dataId));
+        tEnv().createTemporarySystemFunction("f", 
SetSemanticTableFunction.class);
+    }
+
+    @Test
+    void testCreateTableAsSelectOverSetSemanticPtfWithReorderedColumns() 
throws Exception {
+        tEnv().executeSql(
+                        "CREATE TABLE sink (`out`, `a`) WITH ('connector' = 
'values') AS "
+                                + "SELECT * FROM f(r => TABLE source PARTITION 
BY a, i => 1)")
+                .await();
+
+        final ResolvedSchema schema = tEnv().from("sink").getResolvedSchema();
+        assertThat(schema.getColumnNames()).containsExactly("out", "a");
+
+        assertThat(TestValuesTableFactory.getResultsAsStrings("sink"))
+                .containsExactlyInAnyOrder(
+                        "+I[{+I[1, 1, Hi], 1}, 1]",
+                        "+I[{+I[2, 2, Hello], 1}, 2]",
+                        "+I[{+I[3, 2, Hello world], 1}, 3]");
+    }
+}
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/RTASITCase.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/RTASITCase.java
index c1a943d9f74..f416ab30d05 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/RTASITCase.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/RTASITCase.java
@@ -25,6 +25,7 @@ import org.apache.flink.table.catalog.CatalogTable;
 import org.apache.flink.table.catalog.ObjectPath;
 import org.apache.flink.table.catalog.exceptions.TableNotExistException;
 import org.apache.flink.table.planner.factories.TestValuesTableFactory;
+import 
org.apache.flink.table.planner.plan.nodes.exec.stream.ProcessTableFunctionTestUtils.SetSemanticTableFunction;
 import org.apache.flink.table.planner.runtime.utils.StreamingTestBase;
 import org.apache.flink.table.planner.runtime.utils.TestData;
 import org.apache.flink.table.types.AbstractDataType;
@@ -253,6 +254,28 @@ class RTASITCase extends StreamingTestBase {
         verifyCatalogTable(expectCatalogTable, 
getCatalogTable("not_exist_target"));
     }
 
+    @Test
+    void testReplaceTableAsSelectOverSetSemanticPtf() throws Exception {
+        tEnv().createTemporarySystemFunction("f", 
SetSemanticTableFunction.class);
+        tEnv().executeSql(
+                        "REPLACE TABLE target WITH ('connector' = 'values', 
'bounded' = 'true') AS "
+                                + "SELECT * FROM f(r => TABLE source PARTITION 
BY a, i => 1)")
+                .await();
+
+        assertThat(TestValuesTableFactory.getResultsAsStrings("target"))
+                .containsExactlyInAnyOrder(
+                        "+I[1, {+I[1, 1, Hi], 1}]",
+                        "+I[2, {+I[2, 2, Hello], 1}]",
+                        "+I[3, {+I[3, 2, Hello world], 1}]");
+
+        // The partition key `a` is passed through and the PTF appends its 
`out` column.
+        CatalogTable expectCatalogTable =
+                getExpectCatalogTable(
+                        new String[] {"a", "out"},
+                        new AbstractDataType[] {DataTypes.INT(), 
DataTypes.STRING()});
+        verifyCatalogTable(expectCatalogTable, getCatalogTable("target"));
+    }
+
     private CatalogTable getExpectCatalogTable(
             String[] cols, AbstractDataType<?>[] fieldDataTypes) {
         return getExpectCatalogTable(cols, fieldDataTypes, 
getDefaultTargetTableOptions());
diff --git 
a/flink-table/flink-table-planner/src/test/resources/explain/testExplainCtasWithColumnsInCreateAndQueryParts.out
 
b/flink-table/flink-table-planner/src/test/resources/explain/testExplainCtasWithColumnsInCreateAndQueryParts.out
index 811d6694c59..eb7db8e3862 100644
--- 
a/flink-table/flink-table-planner/src/test/resources/explain/testExplainCtasWithColumnsInCreateAndQueryParts.out
+++ 
b/flink-table/flink-table-planner/src/test/resources/explain/testExplainCtasWithColumnsInCreateAndQueryParts.out
@@ -1,14 +1,14 @@
 == Abstract Syntax Tree ==
-LogicalSink(table=[default_catalog.default_database.MyCtasTable], 
fields=[EXPR$0, a, b])
-+- LogicalProject(EXPR$0=[null:INTEGER], a=[$0], b=[$1])
+LogicalSink(table=[default_catalog.default_database.MyCtasTable], 
fields=[votes, a, b, metadata_col])
++- LogicalProject(votes=[null:INTEGER], a=[$0], b=[$1], 
metadata_col=[null:BIGINT])
    +- LogicalTableScan(table=[[default_catalog, default_database, MyTable]])
 
 == Optimized Physical Plan ==
-Sink(table=[default_catalog.default_database.MyCtasTable], fields=[EXPR$0, a, 
b])
-+- Calc(select=[null:INTEGER AS EXPR$0, a, b])
+Sink(table=[default_catalog.default_database.MyCtasTable], fields=[votes, a, 
b, metadata_col])
++- Calc(select=[null:INTEGER AS votes, a, b, null:BIGINT AS metadata_col])
    +- DataStreamScan(table=[[default_catalog, default_database, MyTable]], 
fields=[a, b, c])
 
 == Optimized Execution Plan ==
-Sink(table=[default_catalog.default_database.MyCtasTable], fields=[EXPR$0, a, 
b])
-+- Calc(select=[null:INTEGER AS EXPR$0, a, b])
+Sink(table=[default_catalog.default_database.MyCtasTable], fields=[votes, a, 
b, metadata_col])
++- Calc(select=[null:INTEGER AS votes, a, b, null:BIGINT AS metadata_col])
    +- DataStreamScan(table=[[default_catalog, default_database, MyTable]], 
fields=[a, b, c])
diff --git 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/TableSinkTest.scala
 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/TableSinkTest.scala
index ef9776caedc..5201b1f20bc 100644
--- 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/TableSinkTest.scala
+++ 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/TableSinkTest.scala
@@ -889,17 +889,25 @@ class TableSinkTest extends TableTestBase {
   @Test
   def testExplainCreateTableAsSelectWithColumnsInCreateAndQueryParts(): Unit = 
{
     val actual =
-      util.tableEnv.explainSql("""
-                                 |CREATE TABLE MyCtasTable(`votes` INT, 
`votes_2x` AS `b` * 2)
-                                 | WITH (
-                                 |   'connector' = 'values'
-                                 |) AS
-                                 |  SELECT
-                                 |    `a`,
-                                 |    `b`
-                                 |  FROM
-                                 |    MyTable
-                                 |""".stripMargin)
+      util.tableEnv.explainSql(
+        """
+          |CREATE TABLE MyCtasTable(
+          |  `votes` INT,
+          |  `votes_2x` AS `b` * 2,
+          |  `metadata_col` BIGINT METADATA,
+          |  `virtual_col` STRING METADATA VIRTUAL
+          |)
+          | WITH (
+          |   'connector' = 'values',
+          |   'readable-metadata' = 'metadata_col:BIGINT, virtual_col:STRING',
+          |   'writable-metadata' = 'metadata_col:BIGINT'
+          |) AS
+          |  SELECT
+          |    `a`,
+          |    `b`
+          |  FROM
+          |    MyTable
+          |""".stripMargin)
 
     val expected =
       
TableTestUtil.readFromResource("/explain/testExplainCtasWithColumnsInCreateAndQueryParts.out")
@@ -910,17 +918,25 @@ class TableSinkTest extends TableTestBase {
   @Test
   def testExplainReplaceTableAsSelectWithColumnsInCreateAndQueryParts(): Unit 
= {
     val actual =
-      util.tableEnv.explainSql("""
-                                 |REPLACE TABLE MyCtasTable(`votes` INT, 
`votes_2x` AS `b` * 2)
-                                 | WITH (
-                                 |   'connector' = 'values'
-                                 |) AS
-                                 |  SELECT
-                                 |    `a`,
-                                 |    `b`
-                                 |  FROM
-                                 |    MyTable
-                                 |""".stripMargin)
+      util.tableEnv.explainSql(
+        """
+          |REPLACE TABLE MyCtasTable(
+          |  `votes` INT,
+          |  `votes_2x` AS `b` * 2,
+          |  `metadata_col` BIGINT METADATA,
+          |  `virtual_col` STRING METADATA VIRTUAL
+          |)
+          | WITH (
+          |   'connector' = 'values',
+          |   'readable-metadata' = 'metadata_col:BIGINT, virtual_col:STRING',
+          |   'writable-metadata' = 'metadata_col:BIGINT'
+          |) AS
+          |  SELECT
+          |    `a`,
+          |    `b`
+          |  FROM
+          |    MyTable
+          |""".stripMargin)
 
     // Same as CTAS
     val expected =


Reply via email to