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 a6669809b9f [FLINK-39957][table] Support `EXPLAIN` of `CREATE [OR
ALTER] MATERIALIZED TABLE`
a6669809b9f is described below
commit a6669809b9f54bf1fbee2615e3f4c1bc28a002ed
Author: Ramin Gharib <[email protected]>
AuthorDate: Sun Jul 5 09:30:36 2026 +0200
[FLINK-39957][table] Support `EXPLAIN` of `CREATE [OR ALTER] MATERIALIZED
TABLE`
---
.../MaterializedTableManager.java | 13 +-
.../service/operation/OperationExecutor.java | 5 +-
.../AbstractMaterializedTableStatementITCase.java | 44 ++--
.../service/MaterializedTableConversionITCase.java | 39 +++
.../service/MaterializedTableStatementITCase.java | 290 ++++++++++++++-------
.../src/main/codegen/includes/parserImpls.ftl | 11 +-
.../flink/sql/parser/utils/ParserResource.java | 4 +
.../flink/sql/parser/FlinkSqlParserImplTest.java | 48 ++--
.../table/operations/ModifyOperationVisitor.java | 9 +
.../AlterMaterializedTableAsQueryOperation.java | 6 +-
.../AlterMaterializedTableChangeOperation.java | 33 ++-
.../ConvertTableToMaterializedTableOperation.java | 24 +-
.../CreateMaterializedTableOperation.java | 32 ++-
.../FullAlterMaterializedTableOperation.java | 6 +-
...ializedTableAsQueryOperationValidationTest.java | 2 +-
.../table/planner/connectors/DynamicSinkUtils.java | 42 ++-
.../operations/SqlNodeToOperationConversion.java | 8 +-
.../operations/converters/SqlNodeConvertUtils.java | 3 +-
.../SqlAlterMaterializedTableAsQueryConverter.java | 10 +-
...SqlCreateOrAlterMaterializedTableConverter.java | 44 +++-
.../table/planner/delegation/PlannerBase.scala | 68 ++++-
...erializedTableNodeToOperationConverterTest.java | 42 +++
.../table/planner/plan/stream/sql/ExplainTest.java | 203 +++++++++++++++
.../explain/testExplainAlterMaterializedTable.out | 12 +
.../testExplainConvertTableToMaterializedTable.out | 12 +
.../explain/testExplainCreateMaterializedTable.out | 12 +
.../testExplainCreateOrAlterMaterializedTable.out | 14 +
.../testExplainFullAlterMaterializedTable.out | 14 +
28 files changed, 883 insertions(+), 167 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 02c646440b1..56cf110880a 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
@@ -954,7 +954,8 @@ public class MaterializedTableManager {
new AlterMaterializedTableChangeOperation(
op.getTableIdentifier(),
oldTable -> op.getTableChanges(),
- suspendMaterializedTable);
+ suspendMaterializedTable,
+ op.getSinkModifyQuery());
operationExecutor.callExecutableOperation(
handle, alterMaterializedTableChangeOperation);
@@ -1010,7 +1011,10 @@ public class MaterializedTableManager {
AlterMaterializedTableChangeOperation
alterMaterializedTableChangeOperation =
new AlterMaterializedTableChangeOperation(
- tableIdentifier, oldTable -> tableChanges,
oldMaterializedTable);
+ tableIdentifier,
+ oldTable -> tableChanges,
+ oldMaterializedTable,
+ op.getSinkModifyQuery());
operationExecutor.callExecutableOperation(
handle, alterMaterializedTableChangeOperation);
@@ -1029,7 +1033,10 @@ public class MaterializedTableManager {
AlterMaterializedTableChangeOperation op) {
return new AlterMaterializedTableChangeOperation(
- op.getTableIdentifier(), oldTable -> List.of(),
oldMaterializedTable);
+ op.getTableIdentifier(),
+ oldTable -> List.of(),
+ oldMaterializedTable,
+ op.getSinkModifyQuery());
}
private TableChange.ModifyRefreshHandler generateResetSavepointTableChange(
diff --git
a/flink-table/flink-sql-gateway/src/main/java/org/apache/flink/table/gateway/service/operation/OperationExecutor.java
b/flink-table/flink-sql-gateway/src/main/java/org/apache/flink/table/gateway/service/operation/OperationExecutor.java
index 5735abd6dee..2bfd11eadec 100644
---
a/flink-table/flink-sql-gateway/src/main/java/org/apache/flink/table/gateway/service/operation/OperationExecutor.java
+++
b/flink-table/flink-sql-gateway/src/main/java/org/apache/flink/table/gateway/service/operation/OperationExecutor.java
@@ -494,7 +494,8 @@ public class OperationExecutor {
TableEnvironmentInternal tableEnv, OperationHandle handle,
Operation operation) {
if (operation instanceof EndStatementSetOperation) {
return callEndStatementSetOperation(tableEnv, handle);
- } else if (operation instanceof ModifyOperation) {
+ } else if (operation instanceof ModifyOperation
+ && !(operation instanceof MaterializedTableOperation)) {
sessionContext.addStatementSetOperation((ModifyOperation)
operation);
return ResultFetcher.fromTableResult(handle, TABLE_RESULT_OK,
false);
} else {
@@ -518,7 +519,7 @@ public class OperationExecutor {
} else if (op instanceof EndStatementSetOperation) {
throw new SqlExecutionException(
"No Statement Set to submit. 'END' statement should be
used after 'BEGIN STATEMENT SET'.");
- } else if (op instanceof ModifyOperation) {
+ } else if (op instanceof ModifyOperation && !(op instanceof
MaterializedTableOperation)) {
return callModifyOperations(
tableEnv, handle,
Collections.singletonList((ModifyOperation) op));
} else if (op instanceof CompileAndExecutePlanOperation
diff --git
a/flink-table/flink-sql-gateway/src/test/java/org/apache/flink/table/gateway/AbstractMaterializedTableStatementITCase.java
b/flink-table/flink-sql-gateway/src/test/java/org/apache/flink/table/gateway/AbstractMaterializedTableStatementITCase.java
index 7ec2aad15de..d7e95fbac36 100644
---
a/flink-table/flink-sql-gateway/src/test/java/org/apache/flink/table/gateway/AbstractMaterializedTableStatementITCase.java
+++
b/flink-table/flink-sql-gateway/src/test/java/org/apache/flink/table/gateway/AbstractMaterializedTableStatementITCase.java
@@ -221,24 +221,7 @@ public abstract class
AbstractMaterializedTableStatementITCase {
long timeout = Duration.ofSeconds(20).toMillis();
long pause = Duration.ofSeconds(2).toMillis();
- String dataId = TestValuesTableFactory.registerData(data);
- String sourceDdl =
- String.format(
- "CREATE TABLE IF NOT EXISTS my_source (\n"
- + " order_id BIGINT,\n"
- + " user_id BIGINT,\n"
- + " shop_id BIGINT,\n"
- + " order_created_at STRING\n"
- + ")\n"
- + "WITH (\n"
- + " 'connector' = 'values',\n"
- + " 'bounded' = 'true',\n"
- + " 'data-id' = '%s'\n"
- + ")",
- dataId);
- OperationHandle sourceHandle =
- service.executeStatement(sessionHandle, sourceDdl, -1, new
Configuration());
- awaitOperationTermination(service, sessionHandle, sourceHandle);
+ createBoundedValuesSource(data);
String partitionFields =
partitionFormatter != null && !partitionFormatter.isEmpty()
@@ -290,6 +273,28 @@ public abstract class
AbstractMaterializedTableStatementITCase {
"Failed to verify the data in materialized table.");
}
+ /** Registers a bounded {@code my_source} table backed by empty values
data. */
+ public void createBoundedValuesSource(List<Row> data) throws Exception {
+ String dataId = TestValuesTableFactory.registerData(data);
+ String sourceDDL =
+ String.format(
+ "CREATE TABLE IF NOT EXISTS my_source (\n"
+ + " order_id BIGINT,\n"
+ + " user_id BIGINT,\n"
+ + " shop_id BIGINT,\n"
+ + " order_created_at STRING\n"
+ + ")\n"
+ + "WITH (\n"
+ + " 'connector' = 'values',\n"
+ + " 'bounded' = 'true',\n"
+ + " 'data-id' = '%s'\n"
+ + ")",
+ dataId);
+ OperationHandle sourceHandle =
+ service.executeStatement(sessionHandle, sourceDDL, -1, new
Configuration());
+ awaitOperationTermination(service, sessionHandle, sourceHandle);
+ }
+
public List<RowData> fetchTableData(SessionHandle sessionHandle, String
query) {
OperationHandle queryHandle =
service.executeStatement(sessionHandle, query, -1, new
Configuration());
@@ -337,7 +342,8 @@ public abstract class
AbstractMaterializedTableStatementITCase {
public void dropMaterializedTable(ObjectIdentifier objectIdentifier)
throws Exception {
String dropMaterializedTableDDL =
String.format(
- "DROP MATERIALIZED TABLE %s",
objectIdentifier.asSerializableString());
+ "DROP MATERIALIZED TABLE IF EXISTS %s",
+ objectIdentifier.asSerializableString());
OperationHandle dropMaterializedTableHandle =
service.executeStatement(
sessionHandle, dropMaterializedTableDDL, -1, new
Configuration());
diff --git
a/flink-table/flink-sql-gateway/src/test/java/org/apache/flink/table/gateway/service/MaterializedTableConversionITCase.java
b/flink-table/flink-sql-gateway/src/test/java/org/apache/flink/table/gateway/service/MaterializedTableConversionITCase.java
index 870648e9e96..ce9285f70db 100644
---
a/flink-table/flink-sql-gateway/src/test/java/org/apache/flink/table/gateway/service/MaterializedTableConversionITCase.java
+++
b/flink-table/flink-sql-gateway/src/test/java/org/apache/flink/table/gateway/service/MaterializedTableConversionITCase.java
@@ -57,6 +57,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
+import static org.apache.flink.table.catalog.CatalogBaseTable.TableKind.TABLE;
import static
org.apache.flink.table.catalog.CommonCatalogOptions.TABLE_CATALOG_STORE_KIND;
import static
org.apache.flink.table.factories.FactoryUtil.WORKFLOW_SCHEDULER_TYPE;
import static
org.apache.flink.table.gateway.service.MaterializedTableTestUtils.executeStatement;
@@ -293,6 +294,44 @@ class MaterializedTableConversionITCase {
assertThat(suspendedMaterializedTable.getRefreshStatus()).isSameAs(RefreshStatus.SUSPENDED);
}
+ @Test
+ void testExplainConvertTableToMaterializedTable() throws Exception {
+ // pre-create a regular table that CREATE OR ALTER would convert in
place.
+ String createRegularTableDDL =
+ "CREATE TABLE users_shops (\n"
+ + " user_id BIGINT,\n"
+ + " shop_id BIGINT,\n"
+ + " payment_amount_cents BIGINT\n"
+ + ")\n"
+ + "WITH ('connector' = 'values')";
+ OperationHandle createTableHandle =
+ executeStatement(service, sessionHandle,
createRegularTableDDL);
+ awaitOperationTermination(service, sessionHandle, createTableHandle);
+
+ String explainDDL =
+ "EXPLAIN CREATE OR ALTER MATERIALIZED TABLE users_shops"
+ + " FRESHNESS = INTERVAL '30' SECOND"
+ + " AS SELECT user_id, shop_id, payment_amount_cents
FROM datagenSource";
+
+ OperationHandle explainHandle = executeStatement(service,
sessionHandle, explainDDL);
+ awaitOperationTermination(service, sessionHandle, explainHandle);
+ List<RowData> explainResults = fetchAllResults(service, sessionHandle,
explainHandle);
+ assertThat(explainResults).hasSize(1);
+ assertThat(explainResults.get(0).getString(0).toString())
+ .contains(
+ "== Abstract Syntax Tree ==",
+ "== Optimized Physical Plan ==",
+ "== Optimized Execution Plan ==",
+ "Sink(table=[",
+ "users_shops");
+
+ // EXPLAIN is side-effect-free: the entry is still a regular table,
not converted.
+ assertThat(
+ service.getTable(sessionHandle,
getObjectIdentifier("users_shops"))
+ .getTableKind())
+ .isSameAs(TABLE);
+ }
+
private SessionHandle initializeSession() {
SessionHandle sessionHandle =
service.openSession(defaultSessionEnvironment);
String catalogDDL =
diff --git
a/flink-table/flink-sql-gateway/src/test/java/org/apache/flink/table/gateway/service/MaterializedTableStatementITCase.java
b/flink-table/flink-sql-gateway/src/test/java/org/apache/flink/table/gateway/service/MaterializedTableStatementITCase.java
index 134015c565d..ddc0fcd8500 100644
---
a/flink-table/flink-sql-gateway/src/test/java/org/apache/flink/table/gateway/service/MaterializedTableStatementITCase.java
+++
b/flink-table/flink-sql-gateway/src/test/java/org/apache/flink/table/gateway/service/MaterializedTableStatementITCase.java
@@ -54,11 +54,11 @@ import
org.apache.flink.table.gateway.workflow.EmbeddedRefreshHandler;
import
org.apache.flink.table.gateway.workflow.EmbeddedRefreshHandlerSerializer;
import org.apache.flink.table.gateway.workflow.WorkflowInfo;
import
org.apache.flink.table.gateway.workflow.scheduler.EmbeddedQuartzScheduler;
-import org.apache.flink.table.planner.factories.TestValuesTableFactory;
import org.apache.flink.table.refresh.ContinuousRefreshHandler;
import org.apache.flink.table.refresh.ContinuousRefreshHandlerSerializer;
import org.apache.flink.types.Row;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.quartz.JobDetail;
@@ -99,6 +99,12 @@ import static
org.assertj.core.api.Assertions.assertThatThrownBy;
*/
class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatementITCase {
+ @AfterEach
+ void tearDown() throws Exception {
+ ObjectIdentifier userShopsIdentifier =
getObjectIdentifier("users_shops");
+ dropMaterializedTable(userShopsIdentifier);
+ }
+
@Test
void testCreateMaterializedTableInContinuousMode() throws Exception {
String materializedTableDDL =
@@ -161,9 +167,6 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
long checkpointInterval =
getCheckpointIntervalConfig(restClusterClient,
activeRefreshHandler.getJobId());
assertThat(checkpointInterval).isEqualTo(30 * 1000);
-
- // drop the materialized table
- dropMaterializedTable(userShopsIdentifier);
}
@Test
@@ -244,9 +247,6 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
.containsKey("table.catalog-store.file.path")
.doesNotContainKey(WORKFLOW_SCHEDULER_TYPE.key())
.doesNotContainKey(RESOURCES_DOWNLOAD_DIR.key());
-
- // drop the materialized table
- dropMaterializedTable(userShopsIdentifier);
}
@Test
@@ -316,9 +316,6 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
// default checkpoint interval is 3 minutes
final int expectedCheckpointInterval = 60 * 3 * 1000;
assertThat(checkpointInterval).isEqualTo(expectedCheckpointInterval);
-
- // drop the materialized table
- dropMaterializedTable(userShopsIdentifier);
}
@Test
@@ -370,31 +367,11 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
long actualCheckpointInterval =
getCheckpointIntervalConfig(restClusterClient,
activeRefreshHandler.getJobId());
assertThat(actualCheckpointInterval).isEqualTo(checkpointInterval);
-
- // drop the materialized table
- dropMaterializedTable(userShopsIdentifier);
}
@Test
void testCreateMaterializedTableInFullMode() throws Exception {
- String dataId = TestValuesTableFactory.registerData(List.of());
- String sourceDdl =
- String.format(
- "CREATE TABLE IF NOT EXISTS my_source (\n"
- + " order_id BIGINT,\n"
- + " user_id BIGINT,\n"
- + " shop_id BIGINT,\n"
- + " order_created_at STRING\n"
- + ")\n"
- + "WITH (\n"
- + " 'connector' = 'values',\n"
- + " 'bounded' = 'true',\n"
- + " 'data-id' = '%s'\n"
- + ")",
- dataId);
-
- OperationHandle sourceHandle = executeStatement(sourceDdl);
- awaitOperationTermination(service, sessionHandle, sourceHandle);
+ createBoundedValuesSource(List.of());
String materializedTableDDL =
"CREATE MATERIALIZED TABLE users_shops"
@@ -457,9 +434,6 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
.containsKey("table.catalog-store.file.path")
.doesNotContainKey(WORKFLOW_SCHEDULER_TYPE.key())
.doesNotContainKey(RESOURCES_DOWNLOAD_DIR.key());
-
- // drop the materialized table
- dropMaterializedTable(userShopsIdentifier);
}
@Test
@@ -512,7 +486,7 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
data.add(Row.of(3L, 3L, 3L, "2024-01-02"));
createAndVerifyCreateMaterializedTableWithData(
- "my_materialized_table",
+ "users_shops",
data,
Collections.singletonMap("ds", "yyyy-MM-dd"),
RefreshMode.CONTINUOUS);
@@ -522,7 +496,7 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
long currentTime = System.currentTimeMillis();
String alterStatement =
- "ALTER MATERIALIZED TABLE my_materialized_table REFRESH
PARTITION (ds = '2024-01-02')";
+ "ALTER MATERIALIZED TABLE users_shops REFRESH PARTITION (ds =
'2024-01-02')";
OperationHandle alterHandle = executeStatement(alterStatement);
awaitOperationTermination(service, sessionHandle, alterHandle);
List<RowData> result = fetchAllResults(service, sessionHandle,
alterHandle);
@@ -535,7 +509,7 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
// 2. verify the new job overwrite the data
CommonTestUtils.waitUtil(
() ->
- fetchTableData(sessionHandle, "SELECT * FROM
my_materialized_table").size()
+ fetchTableData(sessionHandle, "SELECT * FROM
users_shops").size()
== data.size(),
Duration.ofMillis(timeout),
Duration.ofMillis(pause),
@@ -543,12 +517,9 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
assertThat(
fetchTableData(
sessionHandle,
- "SELECT * FROM my_materialized_table
where ds = '2024-01-02'")
+ "SELECT * FROM users_shops where ds =
'2024-01-02'")
.size())
.isEqualTo(1);
-
- // drop the materialized table
- dropMaterializedTable(getObjectIdentifier("my_materialized_table"));
}
@Test
@@ -628,9 +599,6 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
"Currently, refreshing materialized table only
supports referring to char, varchar and string type partition keys. All
specified partition keys in partition specs with unsupported types are:\n"
+ "\n"
+ "ds2");
-
- // drop the materialized table
- dropMaterializedTable(userShopsIdentifier);
}
@Test
@@ -736,9 +704,6 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
getJobRestoreSavepointPath(restClusterClient, resumeJobId);
assertThat(actualRestorePath).isNotEmpty();
assertThat(actualRestorePath.get()).isEqualTo(actualSavepointPath);
-
- // drop the materialized table
- dropMaterializedTable(userShopsIdentifier);
}
@Test
@@ -790,9 +755,6 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
.isInstanceOf(ValidationException.class)
.hasMessageContaining(
"Savepoint directory is not configured, can't stop job
with savepoint.");
-
- // drop the materialized table
- dropMaterializedTable(userShopsIdentifier);
}
@Test
@@ -958,9 +920,6 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
fromJson((String)
jobDetail.getJobDataMap().get(WORKFLOW_INFO), WorkflowInfo.class);
assertThat(workflowInfo.getDynamicOptions())
.containsEntry("debezium-json.ignore-parse-errors", "true");
-
- // drop the materialized table
- dropMaterializedTable(userShopsIdentifier);
}
@Test
@@ -1017,8 +976,6 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
String.format(
"Materialized table %s refresh workflow has
been resumed.",
userShopsIdentifier));
-
- dropMaterializedTable(userShopsIdentifier);
}
@Test
@@ -1100,6 +1057,100 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
assertThat(oldTable.getDefinitionFreshness()).isEqualTo(newTable.getDefinitionFreshness());
}
+ @Test
+ void
testCreateOrAlterMaterializedTableColumnListReordersSchemaOnCreatePath() throws
Exception {
+ createBoundedValuesSource(List.of());
+
+ // users_shops does not exist yet, so CREATE OR ALTER falls through to
the create path and
+ // honors the DDL column list order, exactly like a plain CREATE.
+ String materializedTableDDL =
+ "CREATE OR ALTER MATERIALIZED TABLE users_shops (shop_id,
user_id, oca, order_id)"
+ + " WITH(\n"
+ + " 'format' = 'debezium-json'\n"
+ + " )\n"
+ + " AS SELECT \n"
+ + " user_id,\n"
+ + " shop_id,\n"
+ + " order_created_at AS oca,\n"
+ + " order_id"
+ + " FROM my_source";
+ OperationHandle handle = executeStatement(materializedTableDDL);
+ awaitOperationTermination(service, sessionHandle, handle);
+
+ ResolvedCatalogMaterializedTable table =
getTable(getObjectIdentifier("users_shops"));
+ assertThat(table.getResolvedSchema().getColumnNames())
+ .containsExactly("shop_id", "user_id", "oca", "order_id");
+ }
+
+ @Test
+ void testCreateOrAlterMaterializedTableColumnListIgnoredOnAlterPath()
throws Exception {
+ createAndVerifyCreateMaterializedTableWithData(
+ "users_shops", List.of(), Map.of(), RefreshMode.FULL);
+
+ ObjectIdentifier userShopsIdentifier =
getObjectIdentifier("users_shops");
+ ResolvedSchema oldSchema =
getTable(userShopsIdentifier).getResolvedSchema();
+ assertThat(oldSchema.getColumnNames())
+ .containsExactly("user_id", "shop_id", "ds", "order_cnt");
+
+ // users_shops already exists as a materialized table, so CREATE OR
ALTER takes the alter
+ // path. The DDL column list is treated as names only: it does NOT
reorder the schema.
+ String materializedTableDDL =
+ "CREATE OR ALTER MATERIALIZED TABLE users_shops (shop_id,
user_id, ds, order_cnt)"
+ + " PARTITIONED BY (ds)\n"
+ + " WITH(\n"
+ + " 'format' = 'debezium-json'\n"
+ + " )\n"
+ + " AS SELECT \n"
+ + " user_id,\n"
+ + " shop_id,\n"
+ + " ds,\n"
+ + " COUNT(order_id) AS order_cnt\n"
+ + " FROM (\n"
+ + " SELECT user_id, shop_id, order_created_at AS
ds, order_id FROM my_source"
+ + " ) AS tmp\n"
+ + " GROUP BY (user_id, shop_id, ds)";
+ OperationHandle handle = executeStatement(materializedTableDDL);
+ awaitOperationTermination(service, sessionHandle, handle);
+
+ ResolvedSchema newSchema =
getTable(userShopsIdentifier).getResolvedSchema();
+ assertThat(newSchema.getColumnNames())
+ .containsExactly("user_id", "shop_id", "ds", "order_cnt");
+ assertThat(newSchema).isEqualTo(oldSchema);
+ }
+
+ @Test
+ void testAlterMaterializedTableAsQueryRejectsReorder() throws Exception {
+ createAndVerifyCreateMaterializedTableWithData(
+ "users_shops", List.of(), Map.of(), RefreshMode.FULL);
+
+ ObjectIdentifier userShopsIdentifier =
getObjectIdentifier("users_shops");
+
assertThat(getTable(userShopsIdentifier).getResolvedSchema().getColumnNames())
+ .containsExactly("user_id", "shop_id", "ds", "order_cnt");
+
+ // ALTER ... AS re-derives the schema from the new query. Swapping the
first two
+ // projections asks to reorder existing columns. The query-evolution
rules reject this: an
+ // existing materialized table may only append columns at the end, not
reorder them.
+ String alterMaterializedTableAsQueryDDL =
+ "ALTER MATERIALIZED TABLE users_shops"
+ + " AS SELECT \n"
+ + " shop_id,\n"
+ + " user_id,\n"
+ + " ds,\n"
+ + " COUNT(order_id) AS order_cnt\n"
+ + " FROM (\n"
+ + " SELECT user_id, shop_id, order_created_at AS
ds, order_id FROM my_source"
+ + " ) AS tmp\n"
+ + " GROUP BY (user_id, shop_id, ds)";
+ OperationHandle handle =
executeStatement(alterMaterializedTableAsQueryDDL);
+
+ assertThatThrownBy(() -> awaitOperationTermination(service,
sessionHandle, handle))
+ .hasStackTraceContaining("reordering columns are not
supported");
+
+ // The rejected alter leaves the original schema untouched.
+
assertThat(getTable(userShopsIdentifier).getResolvedSchema().getColumnNames())
+ .containsExactly("user_id", "shop_id", "ds", "order_cnt");
+ }
+
@Test
void testCreateMaterializedTableWithDistribution() {
String materializedTableDDL =
@@ -1337,9 +1388,6 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
getJobRestoreSavepointPath(
restClusterClient,
newContinuousRefreshHandler.getJobId());
assertThat(restorePath).isEmpty();
-
- // drop the materialized table
- dropMaterializedTable(userShopsIdentifier);
}
@Test
@@ -1412,9 +1460,6 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
ContinuousRefreshHandler newContinuousRefreshHandler =
getContinuousRefreshHandler(newTable);
assertThat(newContinuousRefreshHandler.getRestorePath()).isEmpty();
-
- // drop the materialized table
- dropMaterializedTable(userShopsIdentifier);
}
@Test
@@ -1618,12 +1663,12 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
data.add(Row.of(2L, 2L, 2L, "2024-01-02"));
createAndVerifyCreateMaterializedTableWithData(
- "my_materialized_table",
+ "users_shops",
data,
Collections.singletonMap("ds", "yyyy-MM-dd"),
RefreshMode.CONTINUOUS);
- ObjectIdentifier objectIdentifier =
getObjectIdentifier("my_materialized_table");
+ ObjectIdentifier objectIdentifier = getObjectIdentifier("users_shops");
// add more data to all data list
data.add(Row.of(3L, 3L, 3L, "2024-01-01"));
@@ -1655,7 +1700,7 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
assertThat(
fetchTableData(
sessionHandle,
- "SELECT * FROM my_materialized_table
where ds = '2024-01-02'")
+ "SELECT * FROM users_shops where ds =
'2024-01-02'")
.size())
.isEqualTo(getPartitionSize(data, "2024-01-02"));
@@ -1663,12 +1708,9 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
assertThat(
fetchTableData(
sessionHandle,
- "SELECT * FROM my_materialized_table
where ds = '2024-01-01'")
+ "SELECT * FROM users_shops where ds =
'2024-01-01'")
.size())
.isNotEqualTo(getPartitionSize(data, "2024-01-01"));
-
- // drop the materialized table
- dropMaterializedTable(objectIdentifier);
}
@Test
@@ -1679,13 +1721,9 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
// create materialized table without partition formatter
createAndVerifyCreateMaterializedTableWithData(
- "my_materialized_table_without_partition_options",
- data,
- Map.of(),
- RefreshMode.CONTINUOUS);
+ "users_shops", data, Map.of(), RefreshMode.CONTINUOUS);
- ObjectIdentifier tableWithoutFormatterIdentifier =
-
getObjectIdentifier("my_materialized_table_without_partition_options");
+ ObjectIdentifier tableWithoutFormatterIdentifier =
getObjectIdentifier("users_shops");
// add more data to all data list
data.add(Row.of(3L, 3L, 3L, "2024-01-01"));
@@ -1719,19 +1757,16 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
assertThat(
fetchTableData(
sessionHandle,
- "SELECT * FROM
my_materialized_table_without_partition_options where ds = '2024-01-01'")
+ "SELECT * FROM users_shops where ds =
'2024-01-01'")
.size())
.isEqualTo(getPartitionSize(data, "2024-01-01"));
assertThat(
fetchTableData(
sessionHandle,
- "SELECT * FROM
my_materialized_table_without_partition_options where ds = '2024-01-02'")
+ "SELECT * FROM users_shops where ds =
'2024-01-02'")
.size())
.isEqualTo(getPartitionSize(data, "2024-01-02"));
-
- // drop the materialized table
- dropMaterializedTable(tableWithoutFormatterIdentifier);
}
@Test
@@ -1740,12 +1775,12 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
// create materialized table with partition formatter
createAndVerifyCreateMaterializedTableWithData(
- "my_materialized_table",
+ "users_shops",
data,
Collections.singletonMap("ds", "yyyy-MM-dd"),
RefreshMode.FULL);
- ObjectIdentifier materializedTableIdentifier =
getObjectIdentifier("my_materialized_table");
+ ObjectIdentifier materializedTableIdentifier =
getObjectIdentifier("users_shops");
// add more data to all data list
data.add(Row.of(1L, 1L, 1L, "2024-01-01"));
@@ -1780,20 +1815,16 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
// data of partition '2024-01-01' is not changed
assertThat(
fetchTableData(
- sessionHandle,
- "SELECT * FROM my_materialized_table where ds
= '2024-01-02'"))
+ sessionHandle, "SELECT * FROM users_shops
where ds = '2024-01-02'"))
.size()
.isEqualTo(getPartitionSize(data, "2024-01-02"));
assertThat(
fetchTableData(
sessionHandle,
- "SELECT * FROM my_materialized_table
where ds = '2024-01-01'")
+ "SELECT * FROM users_shops where ds =
'2024-01-01'")
.size())
.isNotEqualTo(getPartitionSize(data, "2024-01-01"));
-
- // drop the materialized table
- dropMaterializedTable(materializedTableIdentifier);
}
@Test
@@ -1803,12 +1834,12 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
data.add(Row.of(2L, 2L, 2L, "2024-01-02"));
createAndVerifyCreateMaterializedTableWithData(
- "my_materialized_table",
+ "users_shops",
data,
Collections.singletonMap("ds", "yyyy-MM-dd"),
RefreshMode.CONTINUOUS);
- ObjectIdentifier objectIdentifier =
getObjectIdentifier("my_materialized_table");
+ ObjectIdentifier objectIdentifier = getObjectIdentifier("users_shops");
// refresh the materialized table with schedule time not specified
OperationHandle invalidRefreshTableHandle1 =
@@ -1853,9 +1884,86 @@ class MaterializedTableStatementITCase extends
AbstractMaterializedTableStatemen
String.format(
"Failed to parse a valid partition value for
the field 'ds' in materialized table %s using the scheduler time '20240103
00:00:00.000' based on the date format 'yyyy-MM-dd HH:mm:ss'.",
objectIdentifier.asSerializableString()));
+ }
- // drop the materialized table
- dropMaterializedTable(getObjectIdentifier("my_materialized_table"));
+ @Test
+ void testExplainCreateOrAlterMaterializedTableOnExistingTable() throws
Exception {
+ createAndVerifyCreateMaterializedTableWithData(
+ "users_shops", List.of(), Map.of(), RefreshMode.FULL);
+ ObjectIdentifier userShopsIdentifier =
getObjectIdentifier("users_shops");
+ ResolvedCatalogMaterializedTable oldTable =
getTable(userShopsIdentifier);
+
+ // CREATE OR ALTER on an existing table behaves like an
alter-with-query. Re-state the same
+ // definition so EXPLAIN plans the sink over the (unchanged) table.
+ String explainDDL =
+ "EXPLAIN CREATE OR ALTER MATERIALIZED TABLE users_shops"
+ + " PARTITIONED BY (ds)"
+ + " WITH ('format' = 'debezium-json')"
+ + " FRESHNESS = INTERVAL '30' SECOND"
+ + " REFRESH_MODE = FULL"
+ + " AS SELECT"
+ + " user_id,"
+ + " shop_id,"
+ + " ds,"
+ + " COUNT(order_id) AS order_cnt"
+ + " FROM ("
+ + " SELECT user_id, shop_id, order_created_at AS
ds, order_id FROM my_source"
+ + " ) AS tmp"
+ + " GROUP BY (user_id, shop_id, ds)";
+
+ OperationHandle explainHandle = executeStatement(explainDDL);
+ awaitOperationTermination(service, sessionHandle, explainHandle);
+ List<RowData> explainResults = fetchAllResults(service, sessionHandle,
explainHandle);
+ assertThat(explainResults).hasSize(1);
+ assertThat(explainResults.get(0).getString(0).toString())
+ .contains(
+ "== Abstract Syntax Tree ==",
+ "== Optimized Physical Plan ==",
+ "== Optimized Execution Plan ==",
+ "Sink(table=[",
+ "users_shops");
+
+ // EXPLAIN is side-effect-free: the existing materialized table is
unchanged.
+ ResolvedCatalogMaterializedTable newTable =
getTable(userShopsIdentifier);
+
assertThat(newTable.getResolvedSchema()).isEqualTo(oldTable.getResolvedSchema());
+
assertThat(newTable.getExpandedQuery()).isEqualTo(oldTable.getExpandedQuery());
+ }
+
+ @Test
+ void testExplainAlterMaterializedTableAsQuery() throws Exception {
+ createAndVerifyCreateMaterializedTableWithData(
+ "users_shops", List.of(), Map.of(), RefreshMode.FULL);
+ ObjectIdentifier userShopsIdentifier =
getObjectIdentifier("users_shops");
+ ResolvedCatalogMaterializedTable oldTable =
getTable(userShopsIdentifier);
+
+ String explainDDL =
+ "EXPLAIN ALTER MATERIALIZED TABLE users_shops"
+ + " AS SELECT"
+ + " user_id,"
+ + " shop_id,"
+ + " ds,"
+ + " COUNT(order_id) AS order_cnt"
+ + " FROM ("
+ + " SELECT user_id, shop_id, order_created_at AS
ds, order_id FROM my_source"
+ + " ) AS tmp"
+ + " GROUP BY (user_id, shop_id, ds)";
+
+ OperationHandle explainHandle = executeStatement(explainDDL);
+ awaitOperationTermination(service, sessionHandle, explainHandle);
+ List<RowData> explainResults = fetchAllResults(service, sessionHandle,
explainHandle);
+ assertThat(explainResults).hasSize(1);
+ assertThat(explainResults.get(0).getString(0).toString())
+ .contains(
+ "== Abstract Syntax Tree ==",
+ "== Optimized Physical Plan ==",
+ "== Optimized Execution Plan ==",
+ "Sink(table=[",
+ "users_shops");
+
+ // EXPLAIN is side-effect-free: the materialized table definition is
unchanged.
+ ResolvedCatalogMaterializedTable newTable =
getTable(userShopsIdentifier);
+
assertThat(newTable.getResolvedSchema()).isEqualTo(oldTable.getResolvedSchema());
+
assertThat(newTable.getExpandedQuery()).isEqualTo(oldTable.getExpandedQuery());
}
private void setupSavepointDir(Path temporaryPath) throws Exception {
diff --git
a/flink-table/flink-sql-parser/src/main/codegen/includes/parserImpls.ftl
b/flink-table/flink-sql-parser/src/main/codegen/includes/parserImpls.ftl
index aef621ddc7d..2b8250ea98e 100644
--- a/flink-table/flink-sql-parser/src/main/codegen/includes/parserImpls.ftl
+++ b/flink-table/flink-sql-parser/src/main/codegen/includes/parserImpls.ftl
@@ -3138,16 +3138,25 @@ SqlNode SqlRichExplain() :
stmt = RichSqlInsert()
|
stmt = SqlCreate()
+ |
+ stmt = SqlAlterMaterializedTable()
)
{
if ((stmt instanceof SqlCreate)
&& !(stmt instanceof SqlCreateTableAs)
- && !(stmt instanceof SqlReplaceTableAs)) {
+ && !(stmt instanceof SqlReplaceTableAs)
+ && !(stmt instanceof SqlCreateOrAlterMaterializedTable)) {
throw SqlUtil.newContextException(
getPos(),
ParserResource.RESOURCE.explainCreateOrReplaceStatementUnsupported());
}
+ if ((stmt instanceof SqlAlterMaterializedTable) && !(stmt instanceof
SqlAlterMaterializedTableAsQuery)) {
+ throw SqlUtil.newContextException(
+ getPos(),
+
ParserResource.RESOURCE.explainAlterMaterializedTableUnsupported());
+ }
+
return new SqlRichExplain(getPos(), stmt, explainDetails);
}
}
diff --git
a/flink-table/flink-sql-parser/src/main/java/org/apache/flink/sql/parser/utils/ParserResource.java
b/flink-table/flink-sql-parser/src/main/java/org/apache/flink/sql/parser/utils/ParserResource.java
index d459ca28dc8..7dade5ead96 100644
---
a/flink-table/flink-sql-parser/src/main/java/org/apache/flink/sql/parser/utils/ParserResource.java
+++
b/flink-table/flink-sql-parser/src/main/java/org/apache/flink/sql/parser/utils/ParserResource.java
@@ -53,6 +53,10 @@ public interface ParserResource {
"Unsupported CREATE OR REPLACE statement for EXPLAIN. The
statement must define a query using the AS clause (i.e. CTAS/RTAS statements).")
Resources.ExInst<ParseException>
explainCreateOrReplaceStatementUnsupported();
+ @Resources.BaseMessage(
+ "Unsupported ALTER MATERIALIZED TABLE statement for EXPLAIN. The
statement must define a query using the AS clause.")
+ Resources.ExInst<ParseException>
explainAlterMaterializedTableUnsupported();
+
@Resources.BaseMessage(
"Columns identifiers without types in the schema are supported on
CTAS/RTAS statements only.")
Resources.ExInst<ParseException> columnsIdentifiersUnsupported();
diff --git
a/flink-table/flink-sql-parser/src/test/java/org/apache/flink/sql/parser/FlinkSqlParserImplTest.java
b/flink-table/flink-sql-parser/src/test/java/org/apache/flink/sql/parser/FlinkSqlParserImplTest.java
index 764bfce65d4..92f8e34c770 100644
---
a/flink-table/flink-sql-parser/src/test/java/org/apache/flink/sql/parser/FlinkSqlParserImplTest.java
+++
b/flink-table/flink-sql-parser/src/test/java/org/apache/flink/sql/parser/FlinkSqlParserImplTest.java
@@ -35,6 +35,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
import java.util.List;
import java.util.Locale;
@@ -3184,29 +3185,42 @@ class FlinkSqlParserImplTest extends SqlParserTest {
.fails("(?s).*Encountered \"\\,\" at line 1, column 32.\n.*");
}
- @Test
- void testExplainCreateTableNoSupported() {
- this.sql("EXPLAIN CREATE TABLE t (id int^)^")
+ @ParameterizedTest
+ @ValueSource(strings = {"CREATE", "CREATE OR REPLACE"})
+ void testExplainCreateTableNotSupported(final String operation) {
+ this.sql(String.format("EXPLAIN %s TABLE t (id int^)^", operation))
.fails(
"Unsupported CREATE OR REPLACE statement for
EXPLAIN\\. The statement must define a query using the AS clause \\(i\\.e\\.
CTAS/RTAS statements\\)\\.");
}
- @Test
- void testExplainCreateTableAsSelect() {
- this.sql("EXPLAIN CREATE TABLE t AS SELECT * FROM b")
- .ok("EXPLAIN CREATE TABLE `T`\nAS\nSELECT *\nFROM `B`");
- }
-
- @Test
- void testExplainCreateOrReplaceTableAsSelect() {
- this.sql("EXPLAIN CREATE OR REPLACE TABLE t AS SELECT * FROM b")
- .ok("EXPLAIN CREATE OR REPLACE TABLE `T`\nAS\nSELECT *\nFROM
`B`");
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "ALTER MATERIALIZED TABLE t ^SUSPEND^",
+ "ALTER MATERIALIZED TABLE t ^RESUME^",
+ "ALTER MATERIALIZED TABLE t ADD category_id STRING METADATA
^VIRTUAL^",
+ "ALTER MATERIALIZED TABLE t MODIFY measurement double
^METADATA^"
+ })
+ void testExplainAlterMaterializedTableWithoutAsQueryNotSupported(
+ final String alterMaterializedTable) {
+ this.sql(String.format("EXPLAIN %s", alterMaterializedTable))
+ .fails(
+ "Unsupported ALTER MATERIALIZED TABLE statement for
EXPLAIN\\. The statement must define a query using the AS clause.");
}
- @Test
- void testExplainReplaceTableAsSelect() {
- this.sql("EXPLAIN REPLACE TABLE t AS SELECT * FROM b")
- .ok("EXPLAIN REPLACE TABLE `T`\nAS\nSELECT *\nFROM `B`");
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "CREATE",
+ "CREATE OR REPLACE",
+ "REPLACE",
+ "CREATE MATERIALIZED",
+ "ALTER MATERIALIZED",
+ "CREATE OR ALTER MATERIALIZED",
+ })
+ void testExplain(final String operation) {
+ this.sql(String.format("EXPLAIN %s TABLE t AS SELECT * FROM b",
operation))
+ .ok(String.format("EXPLAIN %s TABLE `T`\nAS\nSELECT *\nFROM
`B`", operation));
}
@Test
diff --git
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/ModifyOperationVisitor.java
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/ModifyOperationVisitor.java
index 006f042b893..2fc6d9931cf 100644
---
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/ModifyOperationVisitor.java
+++
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/ModifyOperationVisitor.java
@@ -19,6 +19,9 @@
package org.apache.flink.table.operations;
import org.apache.flink.annotation.Internal;
+import
org.apache.flink.table.operations.materializedtable.AlterMaterializedTableChangeOperation;
+import
org.apache.flink.table.operations.materializedtable.ConvertTableToMaterializedTableOperation;
+import
org.apache.flink.table.operations.materializedtable.CreateMaterializedTableOperation;
/**
* Class that implements visitor pattern. It allows type safe logic on top of
tree of {@link
@@ -39,4 +42,10 @@ public interface ModifyOperationVisitor<T> {
T visit(CreateTableASOperation ctas);
T visit(ReplaceTableAsOperation rtas);
+
+ T visit(CreateMaterializedTableOperation createMaterializedTableOperation);
+
+ T visit(AlterMaterializedTableChangeOperation
alterMaterializedTableChangeOperation);
+
+ T visit(ConvertTableToMaterializedTableOperation
convertTableToMaterializedTableOperation);
}
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 264fc752bfa..9da8302ca60 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
@@ -23,6 +23,7 @@ import
org.apache.flink.table.api.internal.TableResultInternal;
import org.apache.flink.table.catalog.ObjectIdentifier;
import org.apache.flink.table.catalog.ResolvedCatalogMaterializedTable;
import org.apache.flink.table.catalog.TableChange;
+import org.apache.flink.table.operations.QueryOperation;
import java.util.List;
import java.util.function.Function;
@@ -40,8 +41,9 @@ public class AlterMaterializedTableAsQueryOperation extends
AlterMaterializedTab
public AlterMaterializedTableAsQueryOperation(
ObjectIdentifier tableIdentifier,
Function<ResolvedCatalogMaterializedTable, List<TableChange>>
tableChangesForTable,
- ResolvedCatalogMaterializedTable oldTable) {
- super(tableIdentifier, tableChangesForTable, oldTable);
+ ResolvedCatalogMaterializedTable oldTable,
+ QueryOperation sinkModifyQuery) {
+ super(tableIdentifier, tableChangesForTable, oldTable,
sinkModifyQuery);
}
@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 684518e99b0..87d588a920d 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
@@ -37,6 +37,9 @@ import
org.apache.flink.table.catalog.TableChange.ModifyPhysicalColumnType;
import org.apache.flink.table.catalog.TableChange.ModifyRefreshHandler;
import org.apache.flink.table.catalog.TableChange.ModifyRefreshStatus;
import org.apache.flink.table.catalog.TableChange.ModifyStartMode;
+import org.apache.flink.table.operations.ModifyOperation;
+import org.apache.flink.table.operations.ModifyOperationVisitor;
+import org.apache.flink.table.operations.QueryOperation;
import org.apache.flink.table.operations.ddl.AlterTableChangeOperation;
import java.util.ArrayList;
@@ -50,9 +53,11 @@ import java.util.stream.IntStream;
* Alter materialized table with new table definition and table changes
represents the modification.
*/
@Internal
-public class AlterMaterializedTableChangeOperation extends
AlterMaterializedTableOperation {
+public class AlterMaterializedTableChangeOperation extends
AlterMaterializedTableOperation
+ implements ModifyOperation {
private final Function<ResolvedCatalogMaterializedTable,
List<TableChange>> tableChangeForTable;
+ private final QueryOperation sinkModifyQuery;
private ResolvedCatalogMaterializedTable oldTable;
private MaterializedTableChangeHandler handler;
private CatalogMaterializedTable newTable;
@@ -62,9 +67,23 @@ public class AlterMaterializedTableChangeOperation extends
AlterMaterializedTabl
ObjectIdentifier tableIdentifier,
Function<ResolvedCatalogMaterializedTable, List<TableChange>>
tableChangeForTable,
ResolvedCatalogMaterializedTable oldTable) {
+ // Metadata-only changes (options, schema, distribution, ...) carry no
sink-modifying query.
+ this(tableIdentifier, tableChangeForTable, oldTable, null);
+ }
+
+ public AlterMaterializedTableChangeOperation(
+ ObjectIdentifier tableIdentifier,
+ Function<ResolvedCatalogMaterializedTable, List<TableChange>>
tableChangeForTable,
+ ResolvedCatalogMaterializedTable oldTable,
+ QueryOperation sinkModifyQuery) {
super(tableIdentifier);
this.tableChangeForTable = tableChangeForTable;
this.oldTable = oldTable;
+ this.sinkModifyQuery = sinkModifyQuery;
+ }
+
+ public QueryOperation getSinkModifyQuery() {
+ return sinkModifyQuery;
}
public List<TableChange> getTableChanges() {
@@ -76,7 +95,7 @@ public class AlterMaterializedTableChangeOperation extends
AlterMaterializedTabl
public AlterMaterializedTableChangeOperation copyAsTableChangeOperation() {
return new AlterMaterializedTableChangeOperation(
- tableIdentifier, tableChangeForTable, oldTable);
+ tableIdentifier, tableChangeForTable, oldTable,
sinkModifyQuery);
}
public CatalogMaterializedTable getNewTable() {
@@ -144,6 +163,16 @@ public class AlterMaterializedTableChangeOperation extends
AlterMaterializedTabl
"%s %s\n%s", getOperationName(),
tableIdentifier.asSummaryString(), changes);
}
+ @Override
+ public QueryOperation getChild() {
+ return this.sinkModifyQuery;
+ }
+
+ @Override
+ public <T> T accept(final ModifyOperationVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+
/** Hook for subclasses to provide a different new-table builder. */
protected CatalogMaterializedTable computeNewTable() {
return
MaterializedTableChangeHandler.buildNewMaterializedTable(getHandlerWithChanges());
diff --git
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/ConvertTableToMaterializedTableOperation.java
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/ConvertTableToMaterializedTableOperation.java
index 8a83ea0f8e8..f00aef5e9f2 100644
---
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/ConvertTableToMaterializedTableOperation.java
+++
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/ConvertTableToMaterializedTableOperation.java
@@ -26,8 +26,11 @@ import
org.apache.flink.table.catalog.ResolvedCatalogMaterializedTable;
import org.apache.flink.table.catalog.ResolvedCatalogTable;
import org.apache.flink.table.catalog.TableChange;
import org.apache.flink.table.operations.ExecutableOperation;
+import org.apache.flink.table.operations.ModifyOperation;
+import org.apache.flink.table.operations.ModifyOperationVisitor;
import org.apache.flink.table.operations.Operation;
import org.apache.flink.table.operations.OperationUtils;
+import org.apache.flink.table.operations.QueryOperation;
import java.util.LinkedHashMap;
import java.util.List;
@@ -41,24 +44,27 @@ import java.util.stream.Collectors;
*/
@Internal
public class ConvertTableToMaterializedTableOperation
- implements MaterializedTableOperation, ExecutableOperation {
+ implements MaterializedTableOperation, ExecutableOperation,
ModifyOperation {
private final ObjectIdentifier tableIdentifier;
private final ResolvedCatalogTable originalTable;
private final ResolvedCatalogMaterializedTable materializedTable;
private final Function<ResolvedCatalogMaterializedTable, List<TableChange>>
tableChangesForTable;
+ private final QueryOperation sinkModifyQuery;
private List<TableChange> tableChanges;
public ConvertTableToMaterializedTableOperation(
ObjectIdentifier tableIdentifier,
ResolvedCatalogTable originalTable,
ResolvedCatalogMaterializedTable materializedTable,
- Function<ResolvedCatalogMaterializedTable, List<TableChange>>
tableChangesBuilder) {
+ Function<ResolvedCatalogMaterializedTable, List<TableChange>>
tableChangesBuilder,
+ QueryOperation sinkModifyQuery) {
this.tableIdentifier = tableIdentifier;
this.originalTable = originalTable;
this.materializedTable = materializedTable;
this.tableChangesForTable = tableChangesBuilder;
+ this.sinkModifyQuery = sinkModifyQuery;
}
@Override
@@ -81,6 +87,20 @@ public class ConvertTableToMaterializedTableOperation
return materializedTable;
}
+ public QueryOperation getSinkModifyQuery() {
+ return sinkModifyQuery;
+ }
+
+ @Override
+ public QueryOperation getChild() {
+ return sinkModifyQuery;
+ }
+
+ @Override
+ public <T> T accept(ModifyOperationVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+
public List<TableChange> getTableChanges() {
if (tableChanges == null) {
tableChanges = tableChangesForTable.apply(materializedTable);
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 9fb68dc71b0..ff425fe53aa 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
@@ -23,26 +23,33 @@ import org.apache.flink.table.api.internal.TableResultImpl;
import org.apache.flink.table.api.internal.TableResultInternal;
import org.apache.flink.table.catalog.ObjectIdentifier;
import org.apache.flink.table.catalog.ResolvedCatalogMaterializedTable;
+import org.apache.flink.table.operations.ModifyOperation;
+import org.apache.flink.table.operations.ModifyOperationVisitor;
import org.apache.flink.table.operations.Operation;
import org.apache.flink.table.operations.OperationUtils;
+import org.apache.flink.table.operations.QueryOperation;
import org.apache.flink.table.operations.ddl.CreateOperation;
-import java.util.Collections;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
/** Operation to describe a CREATE MATERIALIZED TABLE statement. */
@Internal
public class CreateMaterializedTableOperation
- implements CreateOperation, MaterializedTableOperation {
+ implements CreateOperation, MaterializedTableOperation,
ModifyOperation {
private final ObjectIdentifier tableIdentifier;
private final ResolvedCatalogMaterializedTable materializedTable;
+ private final QueryOperation sinkModifyingQuery;
public CreateMaterializedTableOperation(
- ObjectIdentifier tableIdentifier, ResolvedCatalogMaterializedTable
materializedTable) {
+ ObjectIdentifier tableIdentifier,
+ ResolvedCatalogMaterializedTable materializedTable,
+ QueryOperation sinkModifyQuery) {
this.tableIdentifier = tableIdentifier;
this.materializedTable = materializedTable;
+ this.sinkModifyingQuery = sinkModifyQuery;
}
@Override
@@ -60,6 +67,10 @@ public class CreateMaterializedTableOperation
return materializedTable;
}
+ public QueryOperation getSinkModifyingQuery() {
+ return sinkModifyingQuery;
+ }
+
@Override
public String asSummaryString() {
Map<String, Object> params = new LinkedHashMap<>();
@@ -67,9 +78,16 @@ public class CreateMaterializedTableOperation
params.put("identifier", tableIdentifier);
return OperationUtils.formatWithChildren(
- "CREATE MATERIALIZED TABLE",
- params,
- Collections.emptyList(),
- Operation::asSummaryString);
+ "CREATE MATERIALIZED TABLE", params, List.of(),
Operation::asSummaryString);
+ }
+
+ @Override
+ public QueryOperation getChild() {
+ return this.sinkModifyingQuery;
+ }
+
+ @Override
+ public <T> T accept(final ModifyOperationVisitor<T> visitor) {
+ return visitor.visit(this);
}
}
diff --git
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/FullAlterMaterializedTableOperation.java
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/FullAlterMaterializedTableOperation.java
index ade473ddeac..b524e0c0cbf 100644
---
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/FullAlterMaterializedTableOperation.java
+++
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/operations/materializedtable/FullAlterMaterializedTableOperation.java
@@ -23,6 +23,7 @@ import
org.apache.flink.table.catalog.CatalogMaterializedTable;
import org.apache.flink.table.catalog.ObjectIdentifier;
import org.apache.flink.table.catalog.ResolvedCatalogMaterializedTable;
import org.apache.flink.table.catalog.TableChange;
+import org.apache.flink.table.operations.QueryOperation;
import java.util.List;
import java.util.function.Function;
@@ -42,8 +43,9 @@ public class FullAlterMaterializedTableOperation extends
AlterMaterializedTableC
final Function<ResolvedCatalogMaterializedTable,
List<TableChange>> tableChangeForTable,
final ResolvedCatalogMaterializedTable oldTable,
final Function<ResolvedCatalogMaterializedTable,
CatalogMaterializedTable>
- newTableBuilder) {
- super(tableIdentifier, tableChangeForTable, oldTable);
+ newTableBuilder,
+ final QueryOperation sinkModifyQuery) {
+ super(tableIdentifier, tableChangeForTable, oldTable, sinkModifyQuery);
this.newTableBuilder = newTableBuilder;
}
diff --git
a/flink-table/flink-table-api-java/src/test/java/org/apache/flink/table/operations/materializedtable/AlterMaterializedTableAsQueryOperationValidationTest.java
b/flink-table/flink-table-api-java/src/test/java/org/apache/flink/table/operations/materializedtable/AlterMaterializedTableAsQueryOperationValidationTest.java
index 8a513278bf7..88073daca8b 100644
---
a/flink-table/flink-table-api-java/src/test/java/org/apache/flink/table/operations/materializedtable/AlterMaterializedTableAsQueryOperationValidationTest.java
+++
b/flink-table/flink-table-api-java/src/test/java/org/apache/flink/table/operations/materializedtable/AlterMaterializedTableAsQueryOperationValidationTest.java
@@ -194,6 +194,6 @@ class AlterMaterializedTableAsQueryOperationValidationTest {
private static AlterMaterializedTableAsQueryOperation operation(
ResolvedCatalogMaterializedTable oldTable, List<TableChange>
changes) {
return new AlterMaterializedTableAsQueryOperation(
- ObjectIdentifier.of("cat", "db", "mt"), ignored -> changes,
oldTable);
+ ObjectIdentifier.of("cat", "db", "mt"), ignored -> changes,
oldTable, null);
}
}
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/connectors/DynamicSinkUtils.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/connectors/DynamicSinkUtils.java
index 860b6131da0..cb579c644ca 100644
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/connectors/DynamicSinkUtils.java
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/connectors/DynamicSinkUtils.java
@@ -168,9 +168,9 @@ public final class DynamicSinkUtils {
return convertSinkToRel(
relBuilder,
input,
- Collections.emptyMap(), // dynamicOptions
+ Map.of(), // dynamicOptions
contextResolvedTable,
- Collections.emptyMap(), // staticPartitions
+ Map.of(), // staticPartitions
null, // targetColumns
false,
tableSink,
@@ -192,9 +192,9 @@ public final class DynamicSinkUtils {
return convertSinkToRel(
relBuilder,
input,
- Collections.emptyMap(),
+ Map.of(),
externalModifyOperation.getContextResolvedTable(),
- Collections.emptyMap(),
+ Map.of(),
null, // targetColumns
false,
tableSink,
@@ -249,7 +249,7 @@ public final class DynamicSinkUtils {
return convertSinkToRel(
relBuilder,
input,
- Collections.emptyMap(),
+ Map.of(),
contextResolvedTable,
staticPartitions,
null,
@@ -258,6 +258,36 @@ public final class DynamicSinkUtils {
null); // conflictStrategy
}
+ /**
+ * Converts the {@code AS} query of a materialized table (CREATE / CREATE
OR ALTER / ALTER ...
+ * AS) into a {@link RelNode} that writes into the table, adding helper
projections if
+ * necessary. The caller resolves the target table to its {@link
ResolvedCatalogTable} form (the
+ * new definition for an alter, the created definition for a create).
+ */
+ public static RelNode convertMaterializedTableAsToRel(
+ FlinkRelBuilder relBuilder,
+ RelNode input,
+ Catalog catalog,
+ ObjectIdentifier identifier,
+ ResolvedCatalogTable resolvedTable,
+ Map<String, String> staticPartitions,
+ boolean isOverwrite,
+ DynamicTableSink sink) {
+ final ContextResolvedTable contextResolvedTable =
+ ContextResolvedTable.permanent(identifier, catalog,
resolvedTable);
+
+ return convertSinkToRel(
+ relBuilder,
+ input,
+ Map.of(),
+ contextResolvedTable,
+ staticPartitions,
+ null,
+ isOverwrite,
+ sink,
+ null);
+ }
+
private static RelNode convertSinkToRel(
FlinkRelBuilder relBuilder,
RelNode input,
@@ -1235,7 +1265,7 @@ public final class DynamicSinkUtils {
if (sink instanceof SupportsWritingMetadata) {
return ((SupportsWritingMetadata) sink).listWritableMetadata();
}
- return Collections.emptyMap();
+ return Map.of();
}
private static void validateAndApplyMetadata(
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/SqlNodeToOperationConversion.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/SqlNodeToOperationConversion.java
index eb29049dfb2..074309ab9f4 100644
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/SqlNodeToOperationConversion.java
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/SqlNodeToOperationConversion.java
@@ -32,6 +32,8 @@ import org.apache.flink.sql.parser.ddl.SqlUseDatabase;
import org.apache.flink.sql.parser.ddl.SqlUseModules;
import org.apache.flink.sql.parser.ddl.catalog.SqlDropCatalog;
import org.apache.flink.sql.parser.ddl.catalog.SqlUseCatalog;
+import
org.apache.flink.sql.parser.ddl.materializedtable.SqlAlterMaterializedTableAsQuery;
+import
org.apache.flink.sql.parser.ddl.materializedtable.SqlCreateMaterializedTable;
import org.apache.flink.sql.parser.ddl.table.SqlCreateTableAs;
import org.apache.flink.sql.parser.ddl.table.SqlDropTable;
import org.apache.flink.sql.parser.ddl.view.SqlDropView;
@@ -584,8 +586,10 @@ public class SqlNodeToOperationConversion {
}
} else if (sqlNode.getKind().belongsTo(SqlKind.QUERY)) {
operation = convertSqlQuery(sqlExplain.getStatement());
- } else if ((sqlNode instanceof SqlCreateTableAs)
- || (sqlNode instanceof SqlReplaceTableAs)) {
+ } else if (sqlNode instanceof SqlCreateTableAs
+ || sqlNode instanceof SqlReplaceTableAs
+ || sqlNode instanceof SqlCreateMaterializedTable
+ || sqlNode instanceof SqlAlterMaterializedTableAsQuery) {
operation =
convert(flinkPlanner, catalogManager, sqlNode)
.orElseThrow(
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/SqlNodeConvertUtils.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/SqlNodeConvertUtils.java
index f2a6e2595a1..d2be3838a9c 100644
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/SqlNodeConvertUtils.java
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/SqlNodeConvertUtils.java
@@ -101,7 +101,8 @@ public class SqlNodeConvertUtils {
return OptionalInt.empty();
}
- static PlannerQueryOperation toQueryOperation(SqlNode validated,
ConvertContext context) {
+ public static PlannerQueryOperation toQueryOperation(
+ SqlNode validated, ConvertContext context) {
// transform to a relational tree
RelRoot relational = context.toRelRoot(validated);
return new PlannerQueryOperation(
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/materializedtable/SqlAlterMaterializedTableAsQueryConverter.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/materializedtable/SqlAlterMaterializedTableAsQueryConverter.java
index 3860a3f93c9..eb631523b98 100644
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/materializedtable/SqlAlterMaterializedTableAsQueryConverter.java
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/operations/converters/materializedtable/SqlAlterMaterializedTableAsQueryConverter.java
@@ -26,6 +26,7 @@ import org.apache.flink.table.catalog.ResolvedSchema;
import org.apache.flink.table.catalog.TableChange;
import org.apache.flink.table.operations.Operation;
import
org.apache.flink.table.operations.materializedtable.AlterMaterializedTableAsQueryOperation;
+import org.apache.flink.table.planner.calcite.FlinkPlannerImpl;
import org.apache.flink.table.planner.operations.PlannerQueryOperation;
import
org.apache.flink.table.planner.operations.converters.SqlNodeConvertUtils;
import org.apache.flink.table.planner.utils.MaterializedTableUtils;
@@ -36,6 +37,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
+import static
org.apache.flink.table.planner.operations.converters.SqlNodeConvertUtils.toQueryOperation;
+
/** A converter for {@link SqlAlterMaterializedTableAsQuery}. */
public class SqlAlterMaterializedTableAsQueryConverter
extends
AbstractAlterMaterializedTableConverter<SqlAlterMaterializedTableAsQuery> {
@@ -46,8 +49,13 @@ public class SqlAlterMaterializedTableAsQueryConverter
ResolvedCatalogMaterializedTable oldTable,
ConvertContext context) {
final ObjectIdentifier identifier =
resolveIdentifier(sqlAlterTableAsQuery, context);
+ final FlinkPlannerImpl flinkPlanner = context.getFlinkPlanner();
+ final SqlNode asQuerySqlNode = sqlAlterTableAsQuery.getAsQuery();
+ final SqlNode validatedAsQuery = flinkPlanner.validate(asQuerySqlNode);
+ final PlannerQueryOperation asQuery =
toQueryOperation(validatedAsQuery, context);
+
return new AlterMaterializedTableAsQueryOperation(
- identifier, gatherTableChanges(sqlAlterTableAsQuery, context),
oldTable);
+ identifier, gatherTableChanges(sqlAlterTableAsQuery, context),
oldTable, asQuery);
}
@Override
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 eff024de3d1..286071d3f98 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,6 +45,8 @@ 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;
@@ -59,6 +61,8 @@ 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> {
@@ -121,11 +125,17 @@ 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);
+
return new FullAlterMaterializedTableOperation(
identifier,
currentTable -> buildTableChanges(currentTable, mergeContext,
schemaResolver),
oldTable,
- currentTable -> buildNewTable(currentTable, mergeContext,
schemaResolver));
+ currentTable -> buildNewTable(currentTable, mergeContext,
schemaResolver),
+ asQuery);
}
private Operation handleConvert(
@@ -162,6 +172,19 @@ 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 =
+ new MergeTableAsUtil(context)
+ .maybeRewriteQuery(
+ context.getCatalogManager(),
+ flinkPlanner,
+ asQuery,
+ validatedAsQuery,
+ resolvedNewMaterializedTable);
+
return new ConvertTableToMaterializedTableOperation(
identifier,
oldBaseTable,
@@ -171,7 +194,8 @@ public class SqlCreateOrAlterMaterializedTableConverter
oldBaseTable,
resolvedCatalogMaterializedTable,
baseMergeContext.hasSchemaDefinition(),
- baseMergeContext.hasConstraintDefinition()));
+ baseMergeContext.hasConstraintDefinition()),
+ sinkQuery);
}
private List<TableChange> buildConversionTableChanges(
@@ -237,8 +261,20 @@ public class SqlCreateOrAlterMaterializedTableConverter
final ObjectIdentifier identifier) {
final ResolvedCatalogMaterializedTable resolvedTable =
getResolvedCatalogMaterializedTable(sqlCreateOrAlterTable,
context);
-
- return new CreateMaterializedTableOperation(identifier, resolvedTable);
+ 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 =
+ new MergeTableAsUtil(context)
+ .maybeRewriteQuery(
+ context.getCatalogManager(),
+ flinkPlanner,
+ asQuery,
+ validatedAsQuery,
+ resolvedTable);
+
+ return new CreateMaterializedTableOperation(identifier, resolvedTable,
sinkQuery);
}
private List<TableChange> buildTableChanges(
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 fe17d1f29d8..65c36d73151 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
@@ -35,6 +35,7 @@ import org.apache.flink.table.module.{Module, ModuleManager}
import org.apache.flink.table.operations._
import
org.apache.flink.table.operations.OutputConversionModifyOperation.UpdateMode
import org.apache.flink.table.operations.ddl.CreateTableOperation
+import
org.apache.flink.table.operations.materializedtable.{AlterMaterializedTableChangeOperation,
ConvertTableToMaterializedTableOperation, CreateMaterializedTableOperation}
import org.apache.flink.table.planner.JMap
import org.apache.flink.table.planner.calcite._
import org.apache.flink.table.planner.catalog.CatalogManagerCalciteSchema
@@ -51,9 +52,9 @@ import
org.apache.flink.table.planner.plan.nodes.physical.FlinkPhysicalRel
import org.apache.flink.table.planner.plan.optimize.Optimizer
import org.apache.flink.table.planner.sinks.DataStreamTableSink
import
org.apache.flink.table.planner.sinks.TableSinkUtils.{inferSinkPhysicalSchema,
validateLogicalPhysicalTypesCompatible, validateTableSink}
+import org.apache.flink.table.planner.utils.{JavaScalaConversionUtil,
TableConfigUtils}
import
org.apache.flink.table.planner.utils.InternalConfigOptions.{TABLE_QUERY_CURRENT_DATABASE,
TABLE_QUERY_START_EPOCH_TIME, TABLE_QUERY_START_LOCAL_TIME}
import org.apache.flink.table.planner.utils.JavaScalaConversionUtil.{toJava,
toScala}
-import org.apache.flink.table.planner.utils.TableConfigUtils
import org.apache.flink.table.runtime.generated.CompileUtils
import org.apache.flink.table.types.utils.LegacyTypeInfoDataTypeConverter
@@ -285,8 +286,45 @@ abstract class PlannerBase(
convertCreateTableAsToRel(
rtasOperation.getCreateTableOperation,
rtasOperation.getChild,
- Collections.emptyMap(),
- false)
+ Map.empty[String, String].asJava,
+ isOverwrite = false)
+
+ case createMtOperation: CreateMaterializedTableOperation =>
+ convertMaterializedTableToRel(
+ createMtOperation.getTableIdentifier,
+ createMtOperation.getCatalogMaterializedTable.toResolvedCatalogTable,
+ createMtOperation.getChild,
+ Map.empty[String, String].asJava
+ )
+
+ case alterMtOperation: AlterMaterializedTableChangeOperation
+ if alterMtOperation.getSinkModifyQuery != null =>
+ val newTable =
+
catalogManager.resolveCatalogMaterializedTable(alterMtOperation.getNewTable)
+ // The alter reuses the existing storage, so keep the old table's
resolved connector
+ // options (e.g. managed `path`); new options override.
+ val sinkOptions =
+ new util.HashMap[String,
String](alterMtOperation.getOldTable.getOptions)
+ sinkOptions.putAll(newTable.getOptions)
+ convertMaterializedTableToRel(
+ alterMtOperation.getTableIdentifier,
+ newTable.toResolvedCatalogTable.copy(sinkOptions),
+ alterMtOperation.getChild,
+ Map.empty[String, String].asJava
+ )
+
+ case convertMtOperation: ConvertTableToMaterializedTableOperation
+ if convertMtOperation.getSinkModifyQuery != null =>
+ // The conversion reuses the original table's storage, so keep its
connector options.
+ val sinkOptions =
+ new util.HashMap[String,
String](convertMtOperation.getOriginalTable.getOptions)
+ sinkOptions.putAll(convertMtOperation.getMaterializedTable.getOptions)
+ convertMaterializedTableToRel(
+ convertMtOperation.getTableIdentifier,
+
convertMtOperation.getMaterializedTable.toResolvedCatalogTable.copy(sinkOptions),
+ convertMtOperation.getChild,
+ Map.empty[String, String].asJava
+ )
case stagedSink: StagedSinkModifyOperation =>
val input =
createRelBuilder.queryOperation(modifyOperation.getChild).build()
@@ -528,7 +566,7 @@ abstract class PlannerBase(
factory,
tableIdentifier,
resolvedCatalogTable,
- Collections.emptyMap(),
+ Map.empty[String, String].asJava,
getTableConfig,
getFlinkContext.getClassLoader,
isTemporary)
@@ -558,6 +596,28 @@ abstract class PlannerBase(
tableSink);
}
+ private def convertMaterializedTableToRel(
+ identifier: ObjectIdentifier,
+ resolvedTable: ResolvedCatalogTable,
+ queryOperation: QueryOperation,
+ staticPartitions: JMap[String, String]): RelNode = {
+ val input = createRelBuilder.queryOperation(queryOperation).build()
+ val catalog = toScala(catalogManager.getCatalog(identifier.getCatalogName))
+
+ val tableSink =
+ createDynamicTableSink(identifier, catalog, resolvedTable, isTemporary =
false)
+
+ DynamicSinkUtils.convertMaterializedTableAsToRel(
+ createRelBuilder,
+ input,
+ catalog.orNull,
+ identifier,
+ resolvedTable,
+ staticPartitions,
+ false,
+ tableSink)
+ }
+
protected def createSerdeContext: SerdeContext = {
val planner = createFlinkPlanner
new SerdeContext(
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 6e99aa29ed3..1d2cd769da8 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
@@ -43,6 +43,7 @@ import org.apache.flink.table.catalog.WatermarkSpec;
import org.apache.flink.table.catalog.exceptions.DatabaseNotExistException;
import org.apache.flink.table.catalog.exceptions.TableAlreadyExistException;
import org.apache.flink.table.catalog.exceptions.TableNotExistException;
+import org.apache.flink.table.operations.ExplainOperation;
import org.apache.flink.table.operations.Operation;
import
org.apache.flink.table.operations.materializedtable.AlterMaterializedTableAsQueryOperation;
import
org.apache.flink.table.operations.materializedtable.AlterMaterializedTableChangeOperation;
@@ -51,6 +52,7 @@ import
org.apache.flink.table.operations.materializedtable.AlterMaterializedTabl
import
org.apache.flink.table.operations.materializedtable.AlterMaterializedTableSuspendOperation;
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.utils.TableFunc0;
import org.junit.jupiter.api.BeforeEach;
@@ -757,6 +759,46 @@ class SqlMaterializedTableNodeToOperationConverterTest
assertThat(materializedTable.getOrigin()).isEqualTo(expected);
}
+ @Test
+ void testExplainCreateOrAlterMaterializedTable() {
+ final Operation operation =
+ parse("EXPLAIN CREATE OR ALTER MATERIALIZED TABLE myTable123
AS SELECT 123");
+
+ assertThat(operation).isInstanceOf(ExplainOperation.class);
+ assertThat(((ExplainOperation) operation).getChild())
+ .isInstanceOf(CreateMaterializedTableOperation.class);
+ }
+
+ @Test
+ void testExplainCreateMaterializedTable() {
+ final Operation operation =
+ parse("EXPLAIN CREATE MATERIALIZED TABLE myTable123 AS SELECT
123");
+
+ assertThat(operation).isInstanceOf(ExplainOperation.class);
+ assertThat(((ExplainOperation) operation).getChild())
+ .isInstanceOf(CreateMaterializedTableOperation.class);
+ }
+
+ @Test
+ void testExplainFullAlterMaterializedTable() {
+ // base_mtbl exists so the ALTER path is taken
+ final Operation operation =
+ parse("EXPLAIN CREATE OR ALTER MATERIALIZED TABLE base_mtbl AS
SELECT 123");
+ assertThat(operation).isInstanceOf(ExplainOperation.class);
+ assertThat(((ExplainOperation) operation).getChild())
+ .isInstanceOf(FullAlterMaterializedTableOperation.class);
+ }
+
+ @Test
+ void testExplainAlterMaterializedTable() {
+ final Operation operation =
+ parse("EXPLAIN ALTER MATERIALIZED TABLE base_mtbl AS SELECT
123");
+
+ assertThat(operation).isInstanceOf(ExplainOperation.class);
+ assertThat(((ExplainOperation) operation).getChild())
+ .isInstanceOf(AlterMaterializedTableChangeOperation.class);
+ }
+
private static Collection<TestSpec>
testDataForCreateAlterMaterializedTableFailedCase() {
final Collection<TestSpec> list = new ArrayList<>();
list.addAll(createWithInvalidSchema());
diff --git
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/ExplainTest.java
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/ExplainTest.java
new file mode 100644
index 00000000000..e3b35dd9f6b
--- /dev/null
+++
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/ExplainTest.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.table.planner.plan.stream.sql;
+
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.table.api.TableConfig;
+import org.apache.flink.table.api.config.TableConfigOptions;
+import org.apache.flink.table.planner.utils.StreamTableTestUtil;
+import org.apache.flink.table.planner.utils.TableTestBase;
+import org.apache.flink.table.planner.utils.TableTestUtil;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for EXPLAIN statements. */
+public class ExplainTest extends TableTestBase {
+
+ /**
+ * When the {@code PLAN_TEST_FORCE_OVERWRITE} environment variable is set
{@link #verifyExplain}
+ * overwrites the golden files.
+ */
+ private static final boolean REGENERATE_FILES =
+
"true".equalsIgnoreCase(System.getenv("PLAN_TEST_FORCE_OVERWRITE"));
+
+ private StreamTableTestUtil util;
+ private TestInfo testInfo;
+
+ @BeforeEach
+ void setup(TestInfo testInfo) {
+ this.testInfo = testInfo;
+ this.util = streamTestUtil(TableConfig.getDefault());
+ this.util
+ .getTableEnv()
+ .executeSql(
+ "CREATE TABLE MyTable (\n"
+ + " a INT,\n"
+ + " b BIGINT,\n"
+ + " c STRING\n"
+ + ") WITH (\n"
+ + " 'connector' = 'values'\n"
+ + ")");
+ }
+
+ @Test
+ void testExplainCreateMaterializedTable() {
+ verifyExplain(
+ "CREATE MATERIALIZED TABLE MyMTTable\n"
+ + " WITH (\n"
+ + " 'connector' = 'values'\n"
+ + ") AS\n"
+ + " SELECT\n"
+ + " `a`,\n"
+ + " `b`\n"
+ + " FROM\n"
+ + " MyTable");
+ }
+
+ @Test
+ void testExplainCreateOrAlterMaterializedTable() {
+ verifyExplain(
+ "CREATE OR ALTER MATERIALIZED TABLE MyMTTable (\n"
+ + " `b`,\n"
+ + " `a`\n"
+ + " )\n"
+ + " WITH (\n"
+ + " 'connector' = 'values'\n"
+ + ") AS\n"
+ + " SELECT\n"
+ + " CAST(`a` AS BIGINT) AS `a`,\n"
+ + " `b`\n"
+ + " FROM\n"
+ + " MyTable");
+ }
+
+ @Test
+ void testExplainAlterMaterializedTable() {
+ util.getTableEnv()
+ .executeSql(
+ "CREATE OR ALTER MATERIALIZED TABLE MyMTTable\n"
+ + " WITH (\n"
+ + " 'connector' = 'values'\n"
+ + ") AS\n"
+ + " SELECT\n"
+ + " `a`,\n"
+ + " `b`\n"
+ + " FROM\n"
+ + " MyTable");
+ verifyExplain(
+ "ALTER MATERIALIZED TABLE MyMTTable\n"
+ + "AS\n"
+ + " SELECT\n"
+ + " `a`,\n"
+ + " `b`,\n"
+ + " `c`\n"
+ + " FROM\n"
+ + " MyTable");
+ }
+
+ @Test
+ void testExplainFullAlterMaterializedTable() {
+ util.getTableEnv()
+ .executeSql(
+ "CREATE OR ALTER MATERIALIZED TABLE MyMTTable\n"
+ + " WITH (\n"
+ + " 'connector' = 'values'\n"
+ + ") AS\n"
+ + " SELECT\n"
+ + " `a`,\n"
+ + " `b`\n"
+ + " FROM\n"
+ + " MyTable");
+ verifyExplain(
+ "CREATE OR ALTER MATERIALIZED TABLE MyMTTable(\n"
+ + " `b`,\n"
+ + " `a`,\n"
+ + " `c`\n"
+ + " )\n"
+ + " WITH (\n"
+ + " 'connector' = 'values'\n"
+ + ")\n"
+ + "AS\n"
+ + " SELECT\n"
+ + " CAST(`a` AS BIGINT) AS `a`,\n"
+ + " `b`,\n"
+ + " `c`\n"
+ + " FROM\n"
+ + " MyTable");
+ }
+
+ @Test
+ void testExplainConvertTableToMaterializedTable() {
+ final Configuration rootConfiguration = new Configuration();
+ rootConfiguration.set(
+
TableConfigOptions.MATERIALIZED_TABLE_CONVERSION_FROM_TABLE_ENABLED, true);
+ util.getTableEnv().getConfig().setRootConfiguration(rootConfiguration);
+ util.getTableEnv()
+ .executeSql(
+ "CREATE TABLE MyConvertTable (\n"
+ + " `a` INT,\n"
+ + " `b` BIGINT\n"
+ + ") WITH (\n"
+ + " 'connector' = 'values'\n"
+ + ")");
+ verifyExplain(
+ "CREATE OR ALTER MATERIALIZED TABLE MyConvertTable\n"
+ + " AS\n"
+ + " SELECT\n"
+ + " `a`,\n"
+ + " `b`\n"
+ + " FROM\n"
+ + " MyTable");
+ }
+
+ private void verifyExplain(final String statement) {
+ final String actual = util.getTableEnv().explainSql(statement);
+ final String displayName = this.testInfo.getDisplayName();
+ final String fileName = displayName.substring(0, displayName.length()
- 2) + ".out";
+ if (REGENERATE_FILES) {
+ writeToResource(fileName, actual);
+ return;
+ }
+ final String expected = TableTestUtil.readFromResource("/explain/" +
fileName);
+ assertThat(TableTestUtil.replaceStageId(actual))
+ .isEqualTo(TableTestUtil.replaceStageId(expected));
+ }
+
+ private void writeToResource(final String fileName, final String content) {
+ try {
+ final Path testClassesRoot =
Paths.get(getClass().getResource("/").toURI());
+ final Path resourcesRoot =
+ Paths.get(
+ testClassesRoot
+ .toString()
+ .replace("target/test-classes",
"src/test/resources"));
+
Files.writeString(resourcesRoot.resolve("explain").resolve(fileName), content);
+ } catch (final Exception e) {
+ throw new RuntimeException("Failed to regenerate golden file " +
fileName, e);
+ }
+ }
+}
diff --git
a/flink-table/flink-table-planner/src/test/resources/explain/testExplainAlterMaterializedTable.out
b/flink-table/flink-table-planner/src/test/resources/explain/testExplainAlterMaterializedTable.out
new file mode 100644
index 00000000000..488b651fe05
--- /dev/null
+++
b/flink-table/flink-table-planner/src/test/resources/explain/testExplainAlterMaterializedTable.out
@@ -0,0 +1,12 @@
+== Abstract Syntax Tree ==
+LogicalSink(table=[default_catalog.default_database.MyMTTable], fields=[a, b,
c])
++- LogicalProject(a=[$0], b=[$1], c=[$2])
+ +- LogicalTableScan(table=[[default_catalog, default_database, MyTable]])
+
+== Optimized Physical Plan ==
+Sink(table=[default_catalog.default_database.MyMTTable], fields=[a, b, c])
++- TableSourceScan(table=[[default_catalog, default_database, MyTable]],
fields=[a, b, c])
+
+== Optimized Execution Plan ==
+Sink(table=[default_catalog.default_database.MyMTTable], fields=[a, b, c])
++- TableSourceScan(table=[[default_catalog, default_database, MyTable]],
fields=[a, b, c])
diff --git
a/flink-table/flink-table-planner/src/test/resources/explain/testExplainConvertTableToMaterializedTable.out
b/flink-table/flink-table-planner/src/test/resources/explain/testExplainConvertTableToMaterializedTable.out
new file mode 100644
index 00000000000..dbce86bc35b
--- /dev/null
+++
b/flink-table/flink-table-planner/src/test/resources/explain/testExplainConvertTableToMaterializedTable.out
@@ -0,0 +1,12 @@
+== Abstract Syntax Tree ==
+LogicalSink(table=[default_catalog.default_database.MyConvertTable],
fields=[a, b])
++- LogicalProject(a=[$0], b=[$1])
+ +- LogicalTableScan(table=[[default_catalog, default_database, MyTable]])
+
+== Optimized Physical Plan ==
+Sink(table=[default_catalog.default_database.MyConvertTable], fields=[a, b])
++- TableSourceScan(table=[[default_catalog, default_database, MyTable,
project=[a, b], metadata=[]]], fields=[a, b])
+
+== Optimized Execution Plan ==
+Sink(table=[default_catalog.default_database.MyConvertTable], fields=[a, b])
++- TableSourceScan(table=[[default_catalog, default_database, MyTable,
project=[a, b], metadata=[]]], fields=[a, b])
diff --git
a/flink-table/flink-table-planner/src/test/resources/explain/testExplainCreateMaterializedTable.out
b/flink-table/flink-table-planner/src/test/resources/explain/testExplainCreateMaterializedTable.out
new file mode 100644
index 00000000000..8440931c02e
--- /dev/null
+++
b/flink-table/flink-table-planner/src/test/resources/explain/testExplainCreateMaterializedTable.out
@@ -0,0 +1,12 @@
+== Abstract Syntax Tree ==
+LogicalSink(table=[default_catalog.default_database.MyMTTable], fields=[a, b])
++- LogicalProject(a=[$0], b=[$1])
+ +- LogicalTableScan(table=[[default_catalog, default_database, MyTable]])
+
+== Optimized Physical Plan ==
+Sink(table=[default_catalog.default_database.MyMTTable], fields=[a, b])
++- TableSourceScan(table=[[default_catalog, default_database, MyTable,
project=[a, b], metadata=[]]], fields=[a, b])
+
+== Optimized Execution Plan ==
+Sink(table=[default_catalog.default_database.MyMTTable], fields=[a, b])
++- TableSourceScan(table=[[default_catalog, default_database, MyTable,
project=[a, b], metadata=[]]], fields=[a, b])
diff --git
a/flink-table/flink-table-planner/src/test/resources/explain/testExplainCreateOrAlterMaterializedTable.out
b/flink-table/flink-table-planner/src/test/resources/explain/testExplainCreateOrAlterMaterializedTable.out
new file mode 100644
index 00000000000..3bd4f82b621
--- /dev/null
+++
b/flink-table/flink-table-planner/src/test/resources/explain/testExplainCreateOrAlterMaterializedTable.out
@@ -0,0 +1,14 @@
+== Abstract Syntax Tree ==
+LogicalSink(table=[default_catalog.default_database.MyMTTable], fields=[b, a])
++- LogicalProject(b=[$1], a=[CAST($0):BIGINT])
+ +- LogicalTableScan(table=[[default_catalog, default_database, MyTable]])
+
+== Optimized Physical Plan ==
+Sink(table=[default_catalog.default_database.MyMTTable], fields=[b, a])
++- Calc(select=[b, CAST(a AS BIGINT) AS a])
+ +- TableSourceScan(table=[[default_catalog, default_database, MyTable,
project=[b, a], metadata=[]]], fields=[b, a])
+
+== Optimized Execution Plan ==
+Sink(table=[default_catalog.default_database.MyMTTable], fields=[b, a])
++- Calc(select=[b, CAST(a AS BIGINT) AS a])
+ +- TableSourceScan(table=[[default_catalog, default_database, MyTable,
project=[b, a], metadata=[]]], fields=[b, a])
diff --git
a/flink-table/flink-table-planner/src/test/resources/explain/testExplainFullAlterMaterializedTable.out
b/flink-table/flink-table-planner/src/test/resources/explain/testExplainFullAlterMaterializedTable.out
new file mode 100644
index 00000000000..c5daa68fd09
--- /dev/null
+++
b/flink-table/flink-table-planner/src/test/resources/explain/testExplainFullAlterMaterializedTable.out
@@ -0,0 +1,14 @@
+== Abstract Syntax Tree ==
+LogicalSink(table=[default_catalog.default_database.MyMTTable], fields=[a, b,
c])
++- LogicalProject(a=[CAST($0):BIGINT], b=[$1], c=[$2])
+ +- LogicalTableScan(table=[[default_catalog, default_database, MyTable]])
+
+== Optimized Physical Plan ==
+Sink(table=[default_catalog.default_database.MyMTTable], fields=[a, b, c])
++- Calc(select=[CAST(a AS BIGINT) AS a, b, c])
+ +- TableSourceScan(table=[[default_catalog, default_database, MyTable]],
fields=[a, b, c])
+
+== Optimized Execution Plan ==
+Sink(table=[default_catalog.default_database.MyMTTable], fields=[a, b, c])
++- Calc(select=[CAST(a AS BIGINT) AS a, b, c])
+ +- TableSourceScan(table=[[default_catalog, default_database, MyTable]],
fields=[a, b, c])