MartijnVisser commented on code in PR #174:
URL:
https://github.com/apache/flink-connector-kafka/pull/174#discussion_r4069772654
##########
flink-connector-kafka/src/main/java/org/apache/flink/streaming/connectors/kafka/table/DynamicKafkaDeserializationSchema.java:
##########
@@ -264,7 +267,7 @@ private static final class OutputProjectionCollector
@Override
public void collect(RowData physicalValueRow) {
// no key defined
- if (keyProjection.length == 0) {
+ if (keyProjector.isEmptyProjection()) {
Review Comment:
This used to mean the table has no key, it now also means the query selects
no key column. A record with a null Kafka key is then emitted rather than
skipped, so `SELECT c` returns a row that `SELECT a, c` does not. Can it stay
on `keyDeserialization == null`?
##########
flink-connector-kafka/src/main/java/org/apache/flink/streaming/connectors/kafka/table/KafkaDynamicSource.java:
##########
@@ -74,13 +75,15 @@
import java.util.Properties;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Review Comment:
`mvn spotless:check` fails on this import and on the same one in
DynamicKafkaTableSource, so CI stops before any test runs. Both went unused
when the rebase took main's deletion of `getResetStrategy`.
##########
flink-connector-kafka/src/test/java/org/apache/flink/streaming/connectors/kafka/table/KafkaTableITCase.java:
##########
@@ -1721,6 +1750,504 @@ private void
testStartFromGroupOffsetsWithNoneResetStrategy(final String format)
}
}
+ private void projectionPushdownSetupData(final String format, final String
topic)
+ throws Exception {
+ createTestTopic(topic, 1, 1);
+
+ String groupId = getStandardProps().getProperty("group.id");
+ String bootstraps = getBootstrapServers();
+
+ final String createTable =
+ String.format(
+ "CREATE TABLE kafka (\n"
+ + " `a` STRING,\n"
+ + " `b` STRING,\n"
+ + " `topic` STRING NOT NULL METADATA
VIRTUAL,\n"
+ + " `c` STRING,\n"
+ + " `partition` INT NOT NULL METADATA
VIRTUAL,\n"
+ + " `d` 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"
+ + " %s,\n"
+ + " 'key.fields' = 'a; b',\n"
+ + " %s,\n"
+ + " 'value.fields-include' = 'EXCEPT_KEY'\n"
+ + ")",
+ topic,
+ bootstraps,
+ groupId,
+ keyFormatOptions(format),
+ valueFormatOptions(format));
+ tEnv.executeSql(createTable);
+
+ final String initialValues = "INSERT INTO kafka (a, b, c, d) SELECT
'a', 'b', 'c', 'd'";
+ tEnv.executeSql(initialValues).await();
+ }
+
+ @ParameterizedTest(name = "format: {0}")
+ @MethodSource("formats")
+ public void testProjectionPushdownSelectAllFields(final String format)
throws Exception {
+ final String topic = "testProjectionPushdown_" + format + "_" +
UUID.randomUUID();
+ projectionPushdownSetupData(format, topic);
+
+ assertQueryResult(
+ "SELECT * FROM kafka",
+ "== Optimized Execution Plan ==\n"
+ + "Calc(select=[a, b, topic, c, partition, d])\n"
+ + "+- TableSourceScan(table=[[default_catalog,
default_database, kafka, metadata=[topic, partition]]], fields=[a, b, c, d,
topic, partition])\n",
+ Collections.singletonList(String.format("+I(a,b,%s,c,%d,d)",
topic, 0)));
+
+ cleanupTopic(topic);
+ }
+
+ @ParameterizedTest(name = "format: {0}")
+ @MethodSource("formats")
+ public void testProjectionPushdownSelectSpecificPhysicalFields(final
String format)
Review Comment:
None of these selects only value columns, so the branch is never reached
with a key format present, the case where it differs from before. I think a
case for that, plus one for a null key, would cover it.
##########
flink-connector-kafka/src/main/java/org/apache/flink/streaming/connectors/kafka/table/Decoder.java:
##########
@@ -0,0 +1,471 @@
+/*
+ * 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.streaming.connectors.kafka.table;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.api.common.serialization.DeserializationSchema;
+import org.apache.flink.table.connector.Projection;
+import org.apache.flink.table.connector.format.DecodingFormat;
+import org.apache.flink.table.connector.format.ProjectableDecodingFormat;
+import org.apache.flink.table.connector.source.DynamicTableSource.Context;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
+
+import javax.annotation.Nullable;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * Decoding messages consists of two potential steps:
+ *
+ * <ol>
+ * <li>Deserialization i.e deserializing the {@code byte[]} into a {@link
RowData}. This process
+ * is handled by a {@link DeserializationSchema}.
+ * <li>Projection i.e. projecting any required fields from the deserialized
{@link RowData}
+ * (returned by the {@link DeserializationSchema} in the first step) to
their positions in the
+ * final produced {@link RowData}. This process is handled by a {@link
Projector}.
+ * </ol>
+ *
+ * <p>In order to decode messages correctly, the {@link DeserializationSchema}
and the {@link
+ * Projector} need to work together. For example, the {@link Projector} needs
to know the positions
+ * of the required fields in the {@link RowData} returned by the {@link
DeserializationSchema} in
+ * order to be able to correctly set fields in the final produced {@link
RowData}.
+ *
+ * <p>That's why we have this {@link Decoder} class. This class ensures that
the returned {@link
+ * DeserializationSchema} and {@link Projector} will work together to decode
messages correctly.
+ */
+@Internal
+public class Decoder {
+
+ /**
+ * Can be null. Null is used inside {@link
DynamicKafkaDeserializationSchema} to avoid
+ * deserializing keys if not required.
+ */
+ private final @Nullable DeserializationSchema<RowData>
deserializationSchema;
+
+ /** Mapping of the physical position in the key to the target position in
the RowData. */
+ private final Projector projector;
+
+ private Decoder(
+ final DeserializationSchema<RowData> deserializationSchema, final
Projector projector) {
+ this.deserializationSchema = deserializationSchema;
+ this.projector = projector;
+ }
+
+ /**
+ * @param decodingFormat Optional format for decoding bytes.
+ * @param physicalTableDataType The data type representing the table
schema.
+ * @param physicalDataTypeProjection Indices indicate the position of the
field in the dataType
+ * (key/value). Values indicate the position of the field in the
tableSchema.
+ * @param prefix Optional field prefix
+ * @param projectedPhysicalFields Indices indicate the position of the
field in the produced
+ * Row. Values indicate the position of the field in the table schema.
+ * @param pushProjectionsIntoDecodingFormat if this is true and the format
is a {@link
+ * ProjectableDecodingFormat}, any {@param projectedPhysicalFields}
will be pushed down into
+ * the {@link ProjectableDecodingFormat}. Otherwise, projections will
be applied after
+ * deserialization.
+ * @return a {@link Decoder} instance.
+ */
+ public static Decoder create(
+ final Context context,
+ final @Nullable DecodingFormat<DeserializationSchema<RowData>>
decodingFormat,
+ final DataType physicalTableDataType,
+ final int[] physicalDataTypeProjection,
+ final @Nullable String prefix,
+ final int[][] projectedPhysicalFields,
+ final List<String> metadataKeys,
+ final boolean pushProjectionsIntoDecodingFormat) {
+ if (decodingFormat == null) {
+ return Decoder.noDeserializationOrProjection();
+ } else if (!pushProjectionsIntoDecodingFormat
+ || !(decodingFormat instanceof ProjectableDecodingFormat)) {
+ return Decoder.projectAfterDeserializing(
+ context,
+ decodingFormat,
+ physicalTableDataType,
+ physicalDataTypeProjection,
+ prefix,
+ projectedPhysicalFields,
+ metadataKeys);
+ } else {
+ final ProjectableDecodingFormat<DeserializationSchema<RowData>>
+ projectableDecodingFormat =
+
(ProjectableDecodingFormat<DeserializationSchema<RowData>>)
+ decodingFormat;
+ if (projectableDecodingFormat.supportsNestedProjection()) {
+ return Decoder.projectInsideDeserializer(
+ context,
+ projectableDecodingFormat,
+ physicalTableDataType,
+ physicalDataTypeProjection,
+ prefix,
+ projectedPhysicalFields,
+ metadataKeys);
+ } else {
+ return
Decoder.projectTopLevelInsideDeserializerThenNestedAfter(
+ context,
+ projectableDecodingFormat,
+ physicalTableDataType,
+ physicalDataTypeProjection,
+ prefix,
+ projectedPhysicalFields,
+ metadataKeys);
+ }
+ }
+ }
+
+ /**
+ * @return a {@link DeserializationSchema} or null.
+ */
+ @Nullable
+ public DeserializationSchema<RowData> getDeserializationSchema() {
+ return deserializationSchema;
+ }
+
+ /**
+ * @return a {@link Projector}.
+ */
+ public Projector getProjector() {
+ return projector;
+ }
+
+ /**
+ * Creates an identity projection array where each field in the row type
maps to itself.
+ *
+ * @param rowType the row type representing the table schema
+ * @return an int[][] with one entry per field in the row type, where
entry i is {i}
+ */
+ public static int[][] identityProjection(final RowType rowType) {
+ final int tableSchemaSize = rowType.getFieldCount();
+ final int[][] projectedFields = new int[tableSchemaSize][];
+ for (int i = 0; i < tableSchemaSize; i++) {
+ projectedFields[i] = new int[] {i};
+ }
+ return projectedFields;
+ }
+
+ private static Decoder noDeserializationOrProjection() {
+ return new Decoder(null, new ProjectorImpl(Collections.emptyMap(), 0,
0));
+ }
+
+ private static DataType toPhysicalDataType(
+ final DataType physicalTableDataType,
+ final int[] physicalDataTypeProjection,
+ final @Nullable String prefix) {
+ final DataType temp =
+
Projection.of(physicalDataTypeProjection).project(physicalTableDataType);
+ return Optional.ofNullable(prefix)
+ .map(s -> TableDataTypeUtils.stripRowPrefix(temp, s))
+ .orElse(temp);
+ }
+
+ private static Map<Integer, Integer> tableToDeserializedTopLevelPos(
+ final int[] dataTypeProjection) {
+ final HashMap<Integer, Integer> tableToDeserializedPos = new
HashMap<>();
+ for (int i = 0; i < dataTypeProjection.length; i++) {
+ tableToDeserializedPos.put(dataTypeProjection[i], i);
+ }
+ return tableToDeserializedPos;
+ }
+
+ private static int[] copyArray(final int[] arr) {
+ return Arrays.copyOf(arr, arr.length);
+ }
+
+ private static void addMetadataProjections(
+ final DecodingFormat<?> decodingFormat,
+ final int deserializedSize,
+ final int physicalSize,
+ final List<String> requestedMetadataKeys,
+ final Map<List<Integer>, Integer> deserializedToProducedPos) {
+
+ if (!requestedMetadataKeys.isEmpty()) {
+ decodingFormat.applyReadableMetadata(requestedMetadataKeys);
+
+ // project only requested metadata keys
+ for (int i = 0; i < requestedMetadataKeys.size(); i++) {
+ // metadata is always added to the end of the deserialized row
by the DecodingFormat
+ final int deserializedPos = deserializedSize + i;
+ // we need to always add metadata to the end of the produced
row
+ final int producePos = physicalSize + i;
+ deserializedToProducedPos.put(
+ Collections.singletonList(deserializedPos),
producePos);
+ }
+ }
+ }
+
+ /**
+ * This method generates a {@link Decoder} which pushes projections down
directly into the
+ * {@link ProjectableDecodingFormat} which takes care of projecting the
fields during the
+ * deserialization process itself.
+ */
+ private static Decoder projectInsideDeserializer(
+ final Context context,
+ final ProjectableDecodingFormat<DeserializationSchema<RowData>>
+ projectableDecodingFormat,
+ final DataType physicalTableDataType,
+ final int[] physicalDataTypeProjection,
+ final @Nullable String prefix,
+ final int[][] projectedPhysicalFields,
+ final List<String> metadataKeys) {
+ final Map<Integer, Integer> tableToDeserializedTopLevelPos =
+ tableToDeserializedTopLevelPos(physicalDataTypeProjection);
+
+ final List<int[]> deserializerProjectedFields = new ArrayList<>();
+ final Map<List<Integer>, Integer> deserializedToProducedPos = new
HashMap<>();
+ for (int producedPos = 0; producedPos <
projectedPhysicalFields.length; producedPos++) {
+ final int[] tablePos = projectedPhysicalFields[producedPos];
+ final int tableTopLevelPos = tablePos[0];
+
+ final Integer dataTypeTopLevelPos =
+ tableToDeserializedTopLevelPos.get(tableTopLevelPos);
+ if (dataTypeTopLevelPos != null) {
+ final int[] dataTypePos = copyArray(tablePos);
+ dataTypePos[0] = dataTypeTopLevelPos;
+
+ deserializerProjectedFields.add(dataTypePos);
+
+ final int deserializedPos = deserializerProjectedFields.size()
- 1;
+ deserializedToProducedPos.put(
+ Collections.singletonList(deserializedPos),
producedPos);
+ }
+ }
+
+ addMetadataProjections(
+ projectableDecodingFormat,
+ deserializerProjectedFields.size(),
+ projectedPhysicalFields.length,
+ metadataKeys,
+ deserializedToProducedPos);
+
+ return new Decoder(
+ projectableDecodingFormat.createRuntimeDecoder(
+ context,
+ toPhysicalDataType(
+ physicalTableDataType,
physicalDataTypeProjection, prefix),
+ deserializerProjectedFields.toArray(
+ new
int[deserializerProjectedFields.size()][])),
+ new ProjectorImpl(
+ deserializedToProducedPos,
+ deserializerProjectedFields.size(),
+ metadataKeys.size()));
+ }
+
+ /**
+ * This method generates a {@link Decoder} for a {@link
ProjectableDecodingFormat} that does not
+ * support <em>nested</em> projection. Only the required
<em>top-level</em> fields are pushed
+ * down into the format, and any nested sub-fields are extracted from the
deserialized row
+ * afterward by the {@link Projector}.
+ */
+ private static Decoder projectTopLevelInsideDeserializerThenNestedAfter(
+ final Context context,
+ final ProjectableDecodingFormat<DeserializationSchema<RowData>>
+ projectableDecodingFormat,
+ final DataType physicalTableDataType,
+ final int[] physicalDataTypeProjection,
+ final @Nullable String prefix,
+ final int[][] projectedPhysicalFields,
+ final List<String> metadataKeys) {
+ final Map<Integer, Integer> tableToDeserializedTopLevelPos =
+ tableToDeserializedTopLevelPos(physicalDataTypeProjection);
+
+ // Top-level fields (in data type space) to push into the format,
deduplicated so each
+ // required top-level field is only deserialized once.
+ final List<int[]> topLevelProjectedFields = new ArrayList<>();
+ final Map<Integer, Integer> dataTypeTopLevelToDeserializedPos = new
HashMap<>();
+ final Map<List<Integer>, Integer> deserializedToProducedPos = new
HashMap<>();
+ for (int producedPos = 0; producedPos <
projectedPhysicalFields.length; producedPos++) {
+ final int[] tablePos = projectedPhysicalFields[producedPos];
+ final int tableTopLevelPos = tablePos[0];
+
+ final Integer dataTypeTopLevelPos =
+ tableToDeserializedTopLevelPos.get(tableTopLevelPos);
+ if (dataTypeTopLevelPos != null) {
+ final int deserializedTopLevelPos =
+ dataTypeTopLevelToDeserializedPos.computeIfAbsent(
+ dataTypeTopLevelPos,
+ k -> {
+ topLevelProjectedFields.add(new int[] {k});
+ return topLevelProjectedFields.size() - 1;
+ });
+
+ // The remaining (nested) path is extracted from the
deserialized row afterward.
+ final int[] deserializedPos = copyArray(tablePos);
+ deserializedPos[0] = deserializedTopLevelPos;
+ deserializedToProducedPos.put(
+ Collections.unmodifiableList(
+ Arrays.stream(deserializedPos)
+ .boxed()
+ .collect(Collectors.toList())),
+ producedPos);
+ }
+ }
+
+ addMetadataProjections(
+ projectableDecodingFormat,
+ topLevelProjectedFields.size(),
+ projectedPhysicalFields.length,
+ metadataKeys,
+ deserializedToProducedPos);
+
+ return new Decoder(
+ projectableDecodingFormat.createRuntimeDecoder(
+ context,
+ toPhysicalDataType(
+ physicalTableDataType,
physicalDataTypeProjection, prefix),
+ topLevelProjectedFields.toArray(new
int[topLevelProjectedFields.size()][])),
+ new ProjectorImpl(
+ deserializedToProducedPos,
+ topLevelProjectedFields.size(),
+ metadataKeys.size()));
+ }
+
+ /**
+ * This method generates a {@link Decoder} which deserializes the data
fully using the {@link
+ * DecodingFormat} and then applies any projections afterward.
+ */
+ private static Decoder projectAfterDeserializing(
+ final Context context,
+ final DecodingFormat<DeserializationSchema<RowData>>
decodingFormat,
+ final DataType physicalTableDataType,
+ final int[] physicalDataTypeProjection,
+ final @Nullable String prefix,
+ final int[][] projectedPhysicalFields,
+ final List<String> metadataKeys) {
+ final DataType physicalDataType =
+ toPhysicalDataType(physicalTableDataType,
physicalDataTypeProjection, prefix);
+ final Map<Integer, Integer> tableToDeserializedTopLevelPos =
+ tableToDeserializedTopLevelPos(physicalDataTypeProjection);
+
+ final Map<List<Integer>, Integer> deserializedToProducedPos = new
HashMap<>();
+ for (int producedPos = 0; producedPos <
projectedPhysicalFields.length; producedPos++) {
+ final int[] tablePos = projectedPhysicalFields[producedPos];
+ int tableTopLevelPos = tablePos[0];
+
+ final Integer deserializedTopLevelPos =
+ tableToDeserializedTopLevelPos.get(tableTopLevelPos);
+ if (deserializedTopLevelPos != null) {
+ final int[] deserializedPos = copyArray(tablePos);
+ deserializedPos[0] = deserializedTopLevelPos;
+
+ deserializedToProducedPos.put(
+ Collections.unmodifiableList(
+ Arrays.stream(deserializedPos)
+ .boxed()
+ .collect(Collectors.toList())),
+ producedPos);
+ }
+ }
+
+ addMetadataProjections(
+ decodingFormat,
+ physicalDataTypeProjection.length,
+ projectedPhysicalFields.length,
+ metadataKeys,
+ deserializedToProducedPos);
+
+ return new Decoder(
+ decodingFormat.createRuntimeDecoder(context, physicalDataType),
+ new ProjectorImpl(
+ deserializedToProducedPos,
+ physicalDataTypeProjection.length,
+ metadataKeys.size()));
+ }
+
+ /** Projects fields from the deserialized row to their positions in the
final produced row. */
+ @Internal
+ public interface Projector extends Serializable {
+ /** Returns true if {@link #project} will not project any fields. */
+ boolean isEmptyProjection();
+
+ /**
+ * Returns true if projection is needed i.e. if the produced record is
different from the
+ * deserialized record
+ */
+ boolean isProjectionNeeded();
+
+ /** Copies fields from the deserialized row to their final positions
in the produced row. */
+ void project(final RowData deserialized, final GenericRowData
producedRow);
+ }
+
+ private static class ProjectorImpl implements Projector {
+
+ private final Map<List<Integer>, Integer> deserializedToProducedPos;
+ private final boolean isProjectionNeeded;
+
+ ProjectorImpl(
+ final Map<List<Integer>, Integer> deserializedToProducedPos,
+ final int numDeserializedPhysicalFields,
+ final int numMetadataFields) {
+ this.deserializedToProducedPos = deserializedToProducedPos;
+ this.isProjectionNeeded =
+ !(deserializedToProducedPos.size()
+ == (numDeserializedPhysicalFields +
numMetadataFields)
+ && samePositions(deserializedToProducedPos));
+ }
+
+ private static boolean samePositions(
+ Map<List<Integer>, Integer> deserializedToProducedPos) {
+ return deserializedToProducedPos.entrySet().stream()
+ .allMatch(
+ entry -> {
+ final List<Integer> deserializedPos =
entry.getKey();
+ final List<Integer> producedPos =
+
Collections.singletonList(entry.getValue());
+ return Objects.equals(producedPos,
deserializedPos);
+ });
+ }
+
+ @Override
+ public boolean isEmptyProjection() {
+ return deserializedToProducedPos.isEmpty();
+ }
+
+ @Override
+ public boolean isProjectionNeeded() {
+ return isProjectionNeeded;
+ }
+
+ @Override
+ public void project(final RowData deserialized, final GenericRowData
producedRow) {
Review Comment:
This runs per record for every table with a key or metadata, and by default
for any narrowing query. The map walk allocates 24 to 160 bytes a record where
the `int[]` loop it replaces allocated none, a flat `int[]` path for the top
level entries avoids that.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]