Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ protected Transformation<Object> translateToPlanInternal(
tableSink,
-1,
false,
null,
null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
import org.apache.flink.streaming.api.transformations.PartitionTransformation;
import org.apache.flink.streaming.api.transformations.TransformationWithLineage;
import org.apache.flink.streaming.runtime.partitioner.KeyGroupStreamPartitioner;
import org.apache.flink.table.api.InsertConflictStrategy;
import org.apache.flink.table.api.InsertConflictStrategy.ConflictBehavior;
import org.apache.flink.table.api.TableException;
import org.apache.flink.table.api.config.ExecutionConfigOptions;
import org.apache.flink.table.catalog.ResolvedSchema;
Expand Down Expand Up @@ -84,6 +86,8 @@

import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.annotation.JsonProperty;

import javax.annotation.Nullable;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -146,7 +150,8 @@ protected Transformation<Object> createSinkTransformation(
DynamicTableSink tableSink,
int rowtimeFieldIndex,
boolean upsertMaterialize,
int[] inputUpsertKey) {
int[] inputUpsertKey,
@Nullable InsertConflictStrategy conflictStrategy) {
final ResolvedSchema schema = tableSinkSpec.getContextResolvedTable().getResolvedSchema();
final SinkRuntimeProvider runtimeProvider =
tableSink.getSinkRuntimeProvider(
Expand Down Expand Up @@ -187,6 +192,11 @@ protected Transformation<Object> createSinkTransformation(
Optional<LineageVertex> lineageVertexOpt =
TableLineageUtils.extractLineageDataset(outputObject);

// only add materialization if input has changes, unless the conflict strategy has to
// compare every insert against the row stored under the same primary key
final boolean needMaterialization =
upsertMaterialize && (!inputInsertOnly || detectsDuplicateKeys(conflictStrategy));

Transformation<RowData> sinkTransform =
applyConstraintValidations(inputTransform, config, persistedRowType);

Expand All @@ -199,10 +209,10 @@ protected Transformation<Object> createSinkTransformation(
primaryKeys,
sinkParallelism,
inputParallelism,
upsertMaterialize);
needMaterialization);
}

if (upsertMaterialize) {
if (needMaterialization) {
sinkTransform =
applyUpsertMaterialize(
sinkTransform,
Expand Down Expand Up @@ -246,6 +256,14 @@ protected Transformation<Object> createSinkTransformation(
return transformation;
}

/** Whether the conflict strategy has to detect rows that share a primary key. */
protected static boolean detectsDuplicateKeys(
@Nullable InsertConflictStrategy conflictStrategy) {
return conflictStrategy != null
&& (conflictStrategy.getBehavior() == ConflictBehavior.ERROR
|| conflictStrategy.getBehavior() == ConflictBehavior.NOTHING);
}

/**
* Apply an operator to filter or report error to process not-null values for not-null fields.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
import org.apache.flink.streaming.api.transformations.OneInputTransformation;
import org.apache.flink.table.api.InsertConflictStrategy;
import org.apache.flink.table.api.InsertConflictStrategy.ConflictBehavior;
import org.apache.flink.table.api.TableException;
import org.apache.flink.table.api.config.ExecutionConfigOptions;
import org.apache.flink.table.api.config.ExecutionConfigOptions.RowtimeInserter;
Expand Down Expand Up @@ -291,7 +290,8 @@ protected Transformation<Object> translateToPlanInternal(
tableSink,
rowtimeFieldIndex,
upsertMaterialize,
inputUpsertKey);
inputUpsertKey,
conflictStrategy);
}

@Override
Expand Down Expand Up @@ -359,7 +359,7 @@ protected Transformation<RowData> applyUpsertMaterialize(
// This assigns the current watermark as the timestamp to each record,
// which is required for the WatermarkCompactingSinkMaterializer to work correctly
Transformation<RowData> transformForMaterializer = inputTransform;
if (isErrorOrNothingConflictStrategy()) {
if (detectsDuplicateKeys(conflictStrategy)) {
// Use input parallelism to preserve watermark semantics
transformForMaterializer =
ExecNodeUtil.createOneInputTransformation(
Expand Down Expand Up @@ -410,7 +410,7 @@ private OneInputStreamOperator<RowData, RowData> createSumOperator(
GeneratedHashFunction rowHashFunction) {

// Check if we should use the watermark-compacting materializer for ERROR/NOTHING strategies
if (isErrorOrNothingConflictStrategy()) {
if (detectsDuplicateKeys(conflictStrategy)) {
RowType keyType = RowTypeUtils.projectRowType(physicalRowType, primaryKeys);

return WatermarkCompactingSinkMaterializer.create(
Expand Down Expand Up @@ -450,12 +450,6 @@ private OneInputStreamOperator<RowData, RowData> createSumOperator(
config));
}

private boolean isErrorOrNothingConflictStrategy() {
return conflictStrategy != null
&& (conflictStrategy.getBehavior() == ConflictBehavior.ERROR
|| conflictStrategy.getBehavior() == ConflictBehavior.NOTHING);
}

private static SequencedMultiSetStateConfig createStateConfig(
SinkUpsertMaterializeStrategy strategy,
TimeDomain ttlTimeDomain,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,179 @@ Sink(table=[default_catalog.default_database.sink], fields=[a, b])
]]>
</Resource>
</TestCase>
<TestCase name="testForcedMaterializeWithAppendOnlyInput">
<Resource name="explain">
<![CDATA[== Abstract Syntax Tree ==
LogicalSink(table=[default_catalog.default_database.forcedSink], fields=[a, b])
+- LogicalProject(a=[$0], b=[$1])
+- LogicalTableScan(table=[[default_catalog, default_database, MyTable]])

== Optimized Physical Plan ==
Sink(table=[default_catalog.default_database.forcedSink], fields=[a, b], upsertMaterialize=[true])
+- Calc(select=[a, b])
+- DataStreamScan(table=[[default_catalog, default_database, MyTable]], fields=[a, b, c])

== Optimized Execution Plan ==
Sink(table=[default_catalog.default_database.forcedSink], fields=[a, b], upsertMaterialize=[true])
+- Calc(select=[a, b])
+- DataStreamScan(table=[[default_catalog, default_database, MyTable]], fields=[a, b, c])

== Physical Execution Plan ==
{
"nodes" : [ {
"id" : ,
"type" : "Source: Collection Source",
"pact" : "Data Source",
"contents" : "Source: Collection Source",
"parallelism" : 1
}, {
"id" : ,
"type" : "SourceConversion[]",
"pact" : "Operator",
"contents" : "[]:SourceConversion(table=[default_catalog.default_database.MyTable], fields=[a, b, c])",
"parallelism" : 1,
"predecessors" : [ {
"id" : ,
"ship_strategy" : "FORWARD",
"side" : "second"
} ]
}, {
"id" : ,
"type" : "Calc[]",
"pact" : "Operator",
"contents" : "[]:Calc(select=[a, b])",
"parallelism" : 1,
"predecessors" : [ {
"id" : ,
"ship_strategy" : "FORWARD",
"side" : "second"
} ]
}, {
"id" : ,
"type" : "ConstraintEnforcer[]",
"pact" : "Operator",
"contents" : "[]:ConstraintEnforcer[NotNullEnforcer(fields=[a])]",
"parallelism" : 1,
"predecessors" : [ {
"id" : ,
"ship_strategy" : "FORWARD",
"side" : "second"
} ]
}, {
"id" : ,
"type" : "Sink: forcedSink[]",
"pact" : "Data Sink",
"contents" : "[]:Sink(table=[default_catalog.default_database.forcedSink], fields=[a, b], upsertMaterialize=[true])",
"parallelism" : 1,
"predecessors" : [ {
"id" : ,
"ship_strategy" : "FORWARD",
"side" : "second"
} ]
} ]
}]]>
</Resource>
</TestCase>
<TestCase name="testForcedMaterializeWithUpdatingInput">
<Resource name="explain">
<![CDATA[== Abstract Syntax Tree ==
LogicalSink(table=[default_catalog.default_database.forcedSinkWithCount], fields=[c, EXPR$1])
+- LogicalAggregate(group=[{0}], EXPR$1=[COUNT()])
+- LogicalProject(c=[$2])
+- LogicalTableScan(table=[[default_catalog, default_database, MyTable]])

== Optimized Physical Plan ==
Sink(table=[default_catalog.default_database.forcedSinkWithCount], fields=[c, EXPR$1], upsertMaterialize=[true])
+- GroupAggregate(groupBy=[c], select=[c, COUNT(*) AS EXPR$1])
+- Exchange(distribution=[hash[c]])
+- Calc(select=[c])
+- DataStreamScan(table=[[default_catalog, default_database, MyTable]], fields=[a, b, c])

== Optimized Execution Plan ==
Sink(table=[default_catalog.default_database.forcedSinkWithCount], fields=[c, EXPR$1], upsertMaterialize=[true])
+- GroupAggregate(groupBy=[c], select=[c, COUNT(*) AS EXPR$1])
+- Exchange(distribution=[hash[c]])
+- Calc(select=[c])
+- DataStreamScan(table=[[default_catalog, default_database, MyTable]], fields=[a, b, c])

== Physical Execution Plan ==
{
"nodes" : [ {
"id" : ,
"type" : "Source: Collection Source",
"pact" : "Data Source",
"contents" : "Source: Collection Source",
"parallelism" : 1
}, {
"id" : ,
"type" : "SourceConversion[]",
"pact" : "Operator",
"contents" : "[]:SourceConversion(table=[default_catalog.default_database.MyTable], fields=[a, b, c])",
"parallelism" : 1,
"predecessors" : [ {
"id" : ,
"ship_strategy" : "FORWARD",
"side" : "second"
} ]
}, {
"id" : ,
"type" : "Calc[]",
"pact" : "Operator",
"contents" : "[]:Calc(select=[c])",
"parallelism" : 1,
"predecessors" : [ {
"id" : ,
"ship_strategy" : "FORWARD",
"side" : "second"
} ]
}, {
"id" : ,
"type" : "GroupAggregate[]",
"pact" : "Operator",
"contents" : "[]:GroupAggregate(groupBy=[c], select=[c, COUNT(*) AS EXPR$1])",
"parallelism" : 1,
"predecessors" : [ {
"id" : ,
"ship_strategy" : "HASH",
"side" : "second"
} ]
}, {
"id" : ,
"type" : "ConstraintEnforcer[]",
"pact" : "Operator",
"contents" : "[]:ConstraintEnforcer[NotNullEnforcer(fields=[c])]",
"parallelism" : 1,
"predecessors" : [ {
"id" : ,
"ship_strategy" : "FORWARD",
"side" : "second"
} ]
}, {
"id" : ,
"type" : "SinkMaterializer[]",
"pact" : "Operator",
"contents" : "[]:SinkMaterializer(pk=[c])",
"parallelism" : 1,
"predecessors" : [ {
"id" : ,
"ship_strategy" : "HASH",
"side" : "second"
} ]
}, {
"id" : ,
"type" : "Sink: forcedSinkWithCount[]",
"pact" : "Data Sink",
"contents" : "[]:Sink(table=[default_catalog.default_database.forcedSinkWithCount], fields=[c, EXPR$1], upsertMaterialize=[true])",
"parallelism" : 1,
"predecessors" : [ {
"id" : ,
"ship_strategy" : "FORWARD",
"side" : "second"
} ]
} ]
}]]>
</Resource>
</TestCase>
<TestCase name="testInjectiveCastPreservesUpsertKey">
<Resource name="ast">
<![CDATA[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,51 @@ class TableSinkTest extends TableTestBase {
util.verifyRelPlan(stmtSet, ExplainDetail.CHANGELOG_MODE)
}

@Test
def testForcedMaterializeWithAppendOnlyInput(): Unit = {
util.getStreamEnv.setParallelism(1)
util.tableEnv.getConfig.set(
ExecutionConfigOptions.TABLE_EXEC_SINK_UPSERT_MATERIALIZE,
ExecutionConfigOptions.UpsertMaterialize.FORCE)
util.addTable(s"""
|CREATE TABLE forcedSink (
| `a` INT,
| `b` BIGINT,
| PRIMARY KEY (a) NOT ENFORCED
|) WITH (
| 'connector' = 'values',
| 'sink-insert-only' = 'false'
|)
|""".stripMargin)
val stmtSet = util.tableEnv.createStatementSet()
stmtSet.addInsertSql("INSERT INTO forcedSink SELECT a, b FROM MyTable")
// There is nothing to materialize, so the plan must not contain a SinkMaterializer.
util.verifyExplain(stmtSet, ExplainDetail.JSON_EXECUTION_PLAN)
}

@Test
def testForcedMaterializeWithUpdatingInput(): Unit = {
util.getStreamEnv.setParallelism(1)
util.tableEnv.getConfig.set(
ExecutionConfigOptions.TABLE_EXEC_SINK_UPSERT_MATERIALIZE,
ExecutionConfigOptions.UpsertMaterialize.FORCE)
util.addTable(s"""
|CREATE TABLE forcedSinkWithCount (
| `c` STRING,
| `cnt` BIGINT,
| PRIMARY KEY (c) NOT ENFORCED
|) WITH (
| 'connector' = 'values',
| 'sink-insert-only' = 'false'
|)
|""".stripMargin)
val stmtSet = util.tableEnv.createStatementSet()
stmtSet.addInsertSql(
"INSERT INTO forcedSinkWithCount SELECT c, COUNT(*) FROM MyTable GROUP BY c")
// The upsert key already matches the primary key, so only FORCE asks for a SinkMaterializer.
util.verifyExplain(stmtSet, ExplainDetail.JSON_EXECUTION_PLAN)
}

@Test
def testInjectiveCastPreservesUpsertKey(): Unit = {
// Aggregation produces upsert stream with key (a).
Expand Down