wuchong commented on a change in pull request #13862:
URL: https://github.com/apache/flink/pull/13862#discussion_r515829181



##########
File path: 
flink-table/flink-table-common/src/main/java/org/apache/flink/table/factories/FactoryUtil.java
##########
@@ -102,6 +108,36 @@
                .withDescription("Defines the format identifier for encoding 
data. " +
                        "The identifier is used to discover a suitable format 
factory.");
 
+       public static final ConfigOption<List<String>> KEY_FIELDS = 
ConfigOptions

Review comment:
       Could these options belong to `KafkaOptions`?  
   IMO, they are very Kafka specific features and can't reused by most other 
connectors (e.g. Kinesis).

##########
File path: 
flink-connectors/flink-connector-kafka/src/main/java/org/apache/flink/streaming/connectors/kafka/table/KafkaDynamicSource.java
##########
@@ -131,18 +156,22 @@ public KafkaDynamicSource(
 
        @Override
        public ChangelogMode getChangelogMode() {
-               return this.decodingFormat.getChangelogMode();
+               return valueDecodingFormat.getChangelogMode();
        }
 
        @Override
-       public ScanRuntimeProvider getScanRuntimeProvider(ScanContext 
runtimeProviderContext) {
+       public ScanRuntimeProvider getScanRuntimeProvider(ScanContext context) {
+               final DeserializationSchema<RowData> keyDeserialization =
+                               createDeserialization(context, 
keyDecodingFormat, keyProjection, keyPrefix);
+
                final DeserializationSchema<RowData> valueDeserialization =
-                               
decodingFormat.createRuntimeDecoder(runtimeProviderContext, physicalDataType);
+                               createDeserialization(context, 
valueDecodingFormat, valueProjection, keyPrefix);

Review comment:
       ditto.

##########
File path: 
flink-connectors/flink-connector-kafka/src/test/java/org/apache/flink/streaming/connectors/kafka/table/KafkaTableITCase.java
##########
@@ -383,6 +383,147 @@ public void testKafkaSourceSinkWithMetadata() throws 
Exception {
                deleteTestTopic(topic);
        }
 
+       @Test
+       public void testKafkaSourceSinkWithKeyAndPartialValue() throws 
Exception {
+               if (isLegacyConnector) {
+                       return;
+               }
+               // we always use a different topic name for each parameterized 
topic,
+               // in order to make sure the topic can be created.
+               final String topic = "key_partial_value_topic_" + format;
+               createTestTopic(topic, 1, 1);
+
+               // ---------- Produce an event time stream into Kafka 
-------------------
+               String groupId = standardProps.getProperty("group.id");
+               String bootstraps = 
standardProps.getProperty("bootstrap.servers");
+
+               // k_user_id and user_id have different data types to verify 
the correct mapping,
+               // fields are reordered on purpose
+               final String createTable = String.format(
+                               "CREATE TABLE kafka (\n"
+                                               + "  `k_user_id` BIGINT,\n"
+                                               + "  `name` STRING,\n"
+                                               + "  `timestamp` TIMESTAMP(3) 
METADATA,\n"
+                                               + "  `k_event_id` BIGINT,\n"
+                                               + "  `user_id` INT,\n"
+                                               + "  `payload` STRING\n"
+                                               + ") WITH (\n"
+                                               + "  'connector' = 'kafka',\n"
+                                               + "  'topic' = '%s',\n"
+                                               + "  
'properties.bootstrap.servers' = '%s',\n"
+                                               + "  'properties.group.id' = 
'%s',\n"
+                                               + "  'scan.startup.mode' = 
'earliest-offset',\n"
+                                               + "  'key.format' = '%s',\n"
+                                               + "  'key.fields' = 
'k_event_id; k_user_id',\n"
+                                               + "  'key.fields-prefix' = 
'k_',\n"
+                                               + "  'value.format' = '%s',\n"
+                                               + "  'value.fields-include' = 
'EXCEPT_KEY'\n"
+                                               + ")",
+                               topic,
+                               bootstraps,
+                               groupId,
+                               format,
+                               format);
+
+               tEnv.executeSql(createTable);
+
+               String initialValues = "INSERT INTO kafka\n"
+                               + "VALUES\n"
+                               + " (1, 'name 1', TIMESTAMP '2020-03-08 
13:12:11.123', 100, 1, 'payload 1'),\n"
+                               + " (2, 'name 2', TIMESTAMP '2020-03-09 
13:12:11.123', 101, 2, 'payload 2'),\n"
+                               + " (3, 'name 3', TIMESTAMP '2020-03-10 
13:12:11.123', 102, 3, 'payload 3')";

Review comment:
       minor: how about having differnet values for `user_id`?  This can help 
to verify we can extract correct values when name clashes.

##########
File path: 
flink-connectors/flink-connector-kafka/src/main/java/org/apache/flink/streaming/connectors/kafka/table/KafkaDynamicTableFactory.java
##########
@@ -129,78 +142,183 @@ public DynamicTableSource 
createDynamicTableSource(Context context) {
 
                final DataType physicalDataType = 
context.getCatalogTable().getSchema().toPhysicalRowDataType();
 
+               final int[] keyProjection = 
FactoryUtil.createKeyFormatProjection(tableOptions, physicalDataType);
+
+               final int[] valueProjection = 
FactoryUtil.createValueFormatProjection(tableOptions, physicalDataType);
+
+               final String keyPrefix = 
tableOptions.getOptional(FactoryUtil.KEY_FIELDS_PREFIX).orElse(null);
+
                return createKafkaTableSource(

Review comment:
       We don't need to support different Kafka versions now. Therefore, I 
think we can directly construct the `KafkaDynamicSource` and `KafkaDynamicSink` 
here without the abstracted `createKafkaTableSource` method. 

##########
File path: 
flink-connectors/flink-connector-kafka/src/main/java/org/apache/flink/streaming/connectors/kafka/table/KafkaDynamicSink.java
##########
@@ -83,38 +97,53 @@
        protected final Properties properties;
 
        /** Partitioner to select Kafka partition for each item. */
-       protected final Optional<FlinkKafkaPartitioner<RowData>> partitioner;
+       protected final @Nullable FlinkKafkaPartitioner<RowData> partitioner;
 
        /** Sink commit semantic.*/
        protected final KafkaSinkSemantic semantic;
 
        public KafkaDynamicSink(
                        DataType physicalDataType,
+                       @Nullable EncodingFormat<SerializationSchema<RowData>> 
keyEncodingFormat,
+                       EncodingFormat<SerializationSchema<RowData>> 
valueEncodingFormat,
+                       int[] keyProjection,
+                       int[] valueProjection,
+                       @Nullable String keyPrefix,
                        String topic,
                        Properties properties,
-                       Optional<FlinkKafkaPartitioner<RowData>> partitioner,
-                       EncodingFormat<SerializationSchema<RowData>> 
encodingFormat,
+                       @Nullable FlinkKafkaPartitioner<RowData> partitioner,
                        KafkaSinkSemantic semantic) {
+               // Format attributes
                this.physicalDataType = 
Preconditions.checkNotNull(physicalDataType, "Physical data type must not be 
null.");
+               this.keyEncodingFormat = keyEncodingFormat;
+               this.valueEncodingFormat = 
Preconditions.checkNotNull(valueEncodingFormat, "Value encoding format must not 
be null.");
+               this.keyProjection = Preconditions.checkNotNull(keyProjection, 
"Key projection must not be null.");
+               this.valueProjection = 
Preconditions.checkNotNull(valueProjection, "Value projection must not be 
null.");
+               this.keyPrefix = keyPrefix;
+               // Mutable attributes
                this.metadataKeys = Collections.emptyList();
+               // Kafka-specific attributes
                this.topic = Preconditions.checkNotNull(topic, "Topic must not 
be null.");
                this.properties = Preconditions.checkNotNull(properties, 
"Properties must not be null.");
-               this.partitioner = Preconditions.checkNotNull(partitioner, 
"Partitioner must not be null.");
-               this.encodingFormat = 
Preconditions.checkNotNull(encodingFormat, "Encoding format must not be null.");
+               this.partitioner = partitioner;
                this.semantic = Preconditions.checkNotNull(semantic, "Semantic 
must not be null.");
        }
 
        @Override
        public ChangelogMode getChangelogMode(ChangelogMode requestedMode) {
-               return this.encodingFormat.getChangelogMode();
+               return valueEncodingFormat.getChangelogMode();
        }
 
        @Override
        public SinkRuntimeProvider getSinkRuntimeProvider(Context context) {
+               final SerializationSchema<RowData> keySerialization =
+                               createSerialization(context, keyEncodingFormat, 
keyProjection, keyPrefix);
+
                final SerializationSchema<RowData> valueSerialization =
-                               
this.encodingFormat.createRuntimeEncoder(context, this.physicalDataType);
+                               createSerialization(context, 
valueEncodingFormat, valueProjection, keyPrefix);

Review comment:
       Should use `null` for `prefix` parameter? IIUC, the `keyPrefix` should 
only affects key de/serialization. No matter the key fields are included or 
excluded in the value fields, they shoudn't be stripped by prefix when building 
value de/serialization. For example: 
   
   
   ```sql
   CREATE TABLE kafka (
     key_id BIGINT,
     user_id BIGINT,
     key_name STRING,
     payload STRING
   ) WITH (
     'connector' = 'kafka',
     'key.fields' = 'key_id',
     'key.fields-prefix' = 'key',
     'value.fields-include' = 'EXCEPT_KEY'
   )
   ```
   
   The RowType of value format should be `user_id, key_name, payload` instead 
of `user_id, name, payload`, right?
   
   

##########
File path: 
flink-connectors/flink-connector-kafka/src/main/java/org/apache/flink/streaming/connectors/kafka/table/KafkaDynamicTableFactory.java
##########
@@ -129,78 +142,183 @@ public DynamicTableSource 
createDynamicTableSource(Context context) {
 
                final DataType physicalDataType = 
context.getCatalogTable().getSchema().toPhysicalRowDataType();
 
+               final int[] keyProjection = 
FactoryUtil.createKeyFormatProjection(tableOptions, physicalDataType);
+
+               final int[] valueProjection = 
FactoryUtil.createValueFormatProjection(tableOptions, physicalDataType);
+
+               final String keyPrefix = 
tableOptions.getOptional(FactoryUtil.KEY_FIELDS_PREFIX).orElse(null);
+
                return createKafkaTableSource(
                        physicalDataType,
+                       keyDecodingFormat.orElse(null),
+                       valueDecodingFormat,
+                       keyProjection,
+                       valueProjection,
+                       keyPrefix,
                        KafkaOptions.getSourceTopics(tableOptions),
                        KafkaOptions.getSourceTopicPattern(tableOptions),
                        properties,
-                       decodingFormat,
                        startupOptions.startupMode,
                        startupOptions.specificOffsets,
                        startupOptions.startupTimestampMillis);
        }
 
        @Override
        public DynamicTableSink createDynamicTableSink(Context context) {
-               FactoryUtil.TableFactoryHelper helper = 
FactoryUtil.createTableFactoryHelper(this, context);
+               final TableFactoryHelper helper = 
FactoryUtil.createTableFactoryHelper(this, context);
 
-               ReadableConfig tableOptions = helper.getOptions();
+               final ReadableConfig tableOptions = helper.getOptions();
 
-               EncodingFormat<SerializationSchema<RowData>> encodingFormat = 
helper.discoverEncodingFormat(
-                               SerializationFormatFactory.class,
-                               FactoryUtil.FORMAT);
+               final Optional<EncodingFormat<SerializationSchema<RowData>>> 
keyEncodingFormat =
+                               getKeyEncodingFormat(helper);
+
+               final EncodingFormat<SerializationSchema<RowData>> 
valueEncodingFormat =
+                               getValueEncodingFormat(helper, 
keyEncodingFormat.isPresent());
 
-               // Validate the option values.
-               validateTableSinkOptions(tableOptions);
-               // Validate the option data type.
                helper.validateExcept(PROPERTIES_PREFIX);
 
+               validateTableSinkOptions(tableOptions);
+
                final DataType physicalDataType = 
context.getCatalogTable().getSchema().toPhysicalRowDataType();
 
+               final int[] keyProjection = 
FactoryUtil.createKeyFormatProjection(tableOptions, physicalDataType);
+
+               final int[] valueProjection = 
FactoryUtil.createValueFormatProjection(tableOptions, physicalDataType);
+
+               final String keyPrefix = 
tableOptions.getOptional(FactoryUtil.KEY_FIELDS_PREFIX).orElse(null);
+
                return createKafkaTableSink(
                                physicalDataType,
+                               keyEncodingFormat.orElse(null),
+                               valueEncodingFormat,
+                               keyProjection,
+                               valueProjection,
+                               keyPrefix,
                                tableOptions.get(TOPIC).get(0),
                                
getKafkaProperties(context.getCatalogTable().getOptions()),
-                               getFlinkKafkaPartitioner(tableOptions, 
context.getClassLoader()),
-                               encodingFormat,
+                               getFlinkKafkaPartitioner(tableOptions, 
context.getClassLoader()).orElse(null),
                                getSinkSemantic(tableOptions));
        }
 
        // 
--------------------------------------------------------------------------------------------
 
+       private static Optional<DecodingFormat<DeserializationSchema<RowData>>> 
getKeyDecodingFormat(
+                       TableFactoryHelper helper) {
+               final Optional<DecodingFormat<DeserializationSchema<RowData>>> 
keyDecodingFormat =
+                               helper.discoverOptionalDecodingFormat(
+                                               
DeserializationFormatFactory.class,
+                                               FactoryUtil.KEY_FORMAT);
+               keyDecodingFormat.ifPresent(format -> {
+                       if 
(!format.getChangelogMode().containsOnly(RowKind.INSERT)) {
+                               throw new ValidationException(
+                                               String.format(
+                                                               "A key format 
should only deal with INSERT-only records. "
+                                                                               
+ "But %s has a changelog mode of %s.",
+                                                               
helper.getOptions().get(FactoryUtil.KEY_FORMAT),
+                                                               
format.getChangelogMode()));
+                       }
+               });
+               return keyDecodingFormat;
+       }
+
+       private static Optional<EncodingFormat<SerializationSchema<RowData>>> 
getKeyEncodingFormat(
+                       TableFactoryHelper helper) {
+               final Optional<EncodingFormat<SerializationSchema<RowData>>> 
keyEncodingFormat =
+                               helper.discoverOptionalEncodingFormat(
+                                               
SerializationFormatFactory.class,
+                                               FactoryUtil.KEY_FORMAT);
+               keyEncodingFormat.ifPresent(format -> {
+                       if 
(!format.getChangelogMode().containsOnly(RowKind.INSERT)) {
+                               throw new ValidationException(
+                                               String.format(
+                                                               "A key format 
should only deal with INSERT-only records. "
+                                                                               
+ "But %s has a changelog mode of %s.",
+                                                               
helper.getOptions().get(FactoryUtil.KEY_FORMAT),
+                                                               
format.getChangelogMode()));
+                       }
+               });
+               return keyEncodingFormat;
+       }
+
+       private static DecodingFormat<DeserializationSchema<RowData>> 
getValueDecodingFormat(
+                       TableFactoryHelper helper,
+                       boolean hasKeyFormat) {
+               final ConfigOption<String> valueFormatOption;
+               if (hasKeyFormat) {
+                       valueFormatOption = FactoryUtil.VALUE_FORMAT;
+               } else {
+                       valueFormatOption = FactoryUtil.FORMAT;
+               }

Review comment:
       Is this as expected? Could users use `value.format` without 
`key.format`? I thought `value.format` is a repalcement of `format`.

##########
File path: 
flink-connectors/flink-connector-kafka/src/main/java/org/apache/flink/streaming/connectors/kafka/table/KafkaDynamicSink.java
##########
@@ -83,38 +97,53 @@
        protected final Properties properties;
 
        /** Partitioner to select Kafka partition for each item. */
-       protected final Optional<FlinkKafkaPartitioner<RowData>> partitioner;
+       protected final @Nullable FlinkKafkaPartitioner<RowData> partitioner;
 
        /** Sink commit semantic.*/
        protected final KafkaSinkSemantic semantic;
 
        public KafkaDynamicSink(
                        DataType physicalDataType,
+                       @Nullable EncodingFormat<SerializationSchema<RowData>> 
keyEncodingFormat,
+                       EncodingFormat<SerializationSchema<RowData>> 
valueEncodingFormat,
+                       int[] keyProjection,
+                       int[] valueProjection,
+                       @Nullable String keyPrefix,
                        String topic,
                        Properties properties,
-                       Optional<FlinkKafkaPartitioner<RowData>> partitioner,
-                       EncodingFormat<SerializationSchema<RowData>> 
encodingFormat,
+                       @Nullable FlinkKafkaPartitioner<RowData> partitioner,
                        KafkaSinkSemantic semantic) {
+               // Format attributes
                this.physicalDataType = 
Preconditions.checkNotNull(physicalDataType, "Physical data type must not be 
null.");
+               this.keyEncodingFormat = keyEncodingFormat;
+               this.valueEncodingFormat = 
Preconditions.checkNotNull(valueEncodingFormat, "Value encoding format must not 
be null.");
+               this.keyProjection = Preconditions.checkNotNull(keyProjection, 
"Key projection must not be null.");
+               this.valueProjection = 
Preconditions.checkNotNull(valueProjection, "Value projection must not be 
null.");
+               this.keyPrefix = keyPrefix;
+               // Mutable attributes
                this.metadataKeys = Collections.emptyList();
+               // Kafka-specific attributes
                this.topic = Preconditions.checkNotNull(topic, "Topic must not 
be null.");
                this.properties = Preconditions.checkNotNull(properties, 
"Properties must not be null.");
-               this.partitioner = Preconditions.checkNotNull(partitioner, 
"Partitioner must not be null.");
-               this.encodingFormat = 
Preconditions.checkNotNull(encodingFormat, "Encoding format must not be null.");
+               this.partitioner = partitioner;
                this.semantic = Preconditions.checkNotNull(semantic, "Semantic 
must not be null.");
        }
 
        @Override
        public ChangelogMode getChangelogMode(ChangelogMode requestedMode) {
-               return this.encodingFormat.getChangelogMode();
+               return valueEncodingFormat.getChangelogMode();
        }
 
        @Override
        public SinkRuntimeProvider getSinkRuntimeProvider(Context context) {
+               final SerializationSchema<RowData> keySerialization =
+                               createSerialization(context, keyEncodingFormat, 
keyProjection, keyPrefix);
+
                final SerializationSchema<RowData> valueSerialization =
-                               
this.encodingFormat.createRuntimeEncoder(context, this.physicalDataType);
+                               createSerialization(context, 
valueEncodingFormat, valueProjection, keyPrefix);

Review comment:
       What do you think about:
   
   when `'key.fields-prefix'` is defined, `value.fields-include` shoudn't be 
defined to `ALL`, and all value fields shoudn't be prefixed with  `keyPrefix`. 
   
   I think `'key.fields-prefix'` is only used when users want read out key and 
value parts separately and avoid name clashes.
   
   
   

##########
File path: 
flink-connectors/flink-connector-kafka/src/main/java/org/apache/flink/streaming/connectors/kafka/table/KafkaDynamicTableFactory.java
##########
@@ -129,78 +142,183 @@ public DynamicTableSource 
createDynamicTableSource(Context context) {
 
                final DataType physicalDataType = 
context.getCatalogTable().getSchema().toPhysicalRowDataType();
 
+               final int[] keyProjection = 
FactoryUtil.createKeyFormatProjection(tableOptions, physicalDataType);
+
+               final int[] valueProjection = 
FactoryUtil.createValueFormatProjection(tableOptions, physicalDataType);
+
+               final String keyPrefix = 
tableOptions.getOptional(FactoryUtil.KEY_FIELDS_PREFIX).orElse(null);

Review comment:
       Could we add a validation that the declared key fields should be all 
prefixed with the keyPrefix?

##########
File path: 
flink-table/flink-table-common/src/main/java/org/apache/flink/table/factories/FactoryUtil.java
##########
@@ -102,6 +108,36 @@
                .withDescription("Defines the format identifier for encoding 
data. " +
                        "The identifier is used to discover a suitable format 
factory.");
 
+       public static final ConfigOption<List<String>> KEY_FIELDS = 
ConfigOptions

Review comment:
       Yes, personally, I think they should be moved to Kafka and TestUtils 
too. 

##########
File path: 
flink-connectors/flink-connector-kafka/src/main/java/org/apache/flink/streaming/connectors/kafka/table/KafkaDynamicTableFactory.java
##########
@@ -129,78 +142,183 @@ public DynamicTableSource 
createDynamicTableSource(Context context) {
 
                final DataType physicalDataType = 
context.getCatalogTable().getSchema().toPhysicalRowDataType();
 
+               final int[] keyProjection = 
FactoryUtil.createKeyFormatProjection(tableOptions, physicalDataType);
+
+               final int[] valueProjection = 
FactoryUtil.createValueFormatProjection(tableOptions, physicalDataType);
+
+               final String keyPrefix = 
tableOptions.getOptional(FactoryUtil.KEY_FIELDS_PREFIX).orElse(null);
+
                return createKafkaTableSource(

Review comment:
       I see. 




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

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


Reply via email to