laughingman7743 commented on code in PR #28498:
URL: https://github.com/apache/flink/pull/28498#discussion_r4431209368


##########
flink-formats/flink-avro-confluent-registry/src/main/java/org/apache/flink/formats/avro/registry/confluent/debezium/DebeziumAvroDeserializationSchema.java:
##########
@@ -133,30 +262,48 @@ public void deserialize(byte[] message, 
Collector<RowData> out) throws IOExcepti
             return;
         }
         try {
+            // Initialize generic deserializer with the first record's runtime 
writer schema

Review Comment:
   Thanks for driving the read-as-written approach. It is what makes 
`source.properties` carry connector-specific fields, and I would like to use it 
for PostgreSQL `source.lsn` and `source.sequence`, which a downstream CDC sink 
uses to order changes. With this PR applied to current master, a 
PostgreSQL-shaped envelope that requests only `source.properties` returns 
`lsn`, `sequence` and `txId` as expected.
   
   One problem remains with this initialization. The generic reader schema is 
fixed to the writer schema of the first record this instance sees, and 
`RegistryAvroDeserializationSchema` resolves every later record's writer schema 
against it. Once the subject gains a new schema version (a table column change, 
or a Debezium upgrade that adds `source` fields such as `ts_us`/`ts_ns` in 
2.6), later records are no longer read as written:
   
   - A `source` field that only the later version has is dropped from the map, 
without an error, until the job restarts.
   - A field that is required in the first version and absent from the later 
one fails resolution. For example, a NOT NULL column dropped from the database 
table but never declared in the Flink DDL gives `AvroTypeException: Found 
pg.public.orders.Value, expecting pg.public.orders.Value, missing required 
field legacy`, rethrown as `Can't deserialize Debezium Avro message.`
   
   The second case fails only when a metadata column is declared. Without one, 
the same two records deserialize, because the typed path resolves each writer 
schema against the DDL's reader schema.
   
   I reproduced both by registering two versions under one subject in 
`MockSchemaRegistryClient` and passing a v1 record and then a v2 record through 
the same deserializer instance (test below).
   
   Reading each message with its own writer schema fixes both cases. With the 
change below, the existing `DebeziumAvroSerDeSchemaTest` and 
`DebeziumAvroFormatFactoryTest` (12 tests) and the reproduction tests all pass:
   
   ```java
   private transient SchemaCoder writerCoder;
   private transient Map<Schema, GenericDatumReader<GenericRecord>> 
writerReaders;
   private transient BinaryDecoder writerDecoder;
   
   // in deserialize(), replacing the lazily created genericDeserializer
   if (hasMetadata) {
       if (writerCoder == null) {
           writerCoder = coderProvider.get();
           writerReaders = new HashMap<>();
       }
       ByteArrayInputStream in = new ByteArrayInputStream(message);
       Schema writerSchema = writerCoder.readSchema(in);
       GenericDatumReader<GenericRecord> reader =
               writerReaders.computeIfAbsent(writerSchema, 
GenericDatumReader::new);
       writerDecoder = DecoderFactory.get().binaryDecoder(in, writerDecoder);
       GenericRecord envelope = reader.read(null, writerDecoder);
       GenericRecord sourceRecord = (GenericRecord) envelope.get("source");
       this.cachedSourceMap = convertSourceToMap(sourceRecord);
   }
   ```
   
   `ConfluentSchemaRegistryCoder` already caches schemas by id, so the added 
per-message cost is the five-byte header read and a map lookup. 
`genericDeserializer` and `initContext` are then unused.
   
   <details>
   <summary>Reproduction test (same package, imports omitted; uses the 
@VisibleForTesting constructor as the existing metadata tests do)</summary>
   
   ```java
   class Flink20454ReproTest {
   
       private static final String SUBJECT = "pg.public.orders-value";
   
       /** Table DDL: id INT NOT NULL, name STRING. */
       private static final DataType PHYSICAL =
               DataTypes.ROW(
                       DataTypes.FIELD("id", DataTypes.INT().notNull()),
                       DataTypes.FIELD("name", DataTypes.STRING()));
   
       private final MockSchemaRegistryClient client = new 
MockSchemaRegistryClient();
       private final ConfluentSchemaRegistryCoder coder =
               new ConfluentSchemaRegistryCoder(SUBJECT, client);
   
       // ---------------------------------------------------------------- 
schemas
   
       private static Schema value(boolean withLegacyRequiredColumn) {
           SchemaBuilder.FieldAssembler<Schema> f =
                   SchemaBuilder.record("Value")
                           .namespace("pg.public.orders")
                           .fields()
                           .requiredInt("id")
                           .optionalString("name");
           if (withLegacyRequiredColumn) {
               f = f.requiredInt("legacy");
           }
           return f.endRecord();
       }
   
       private static Schema source(boolean withTsUs) {
           SchemaBuilder.FieldAssembler<Schema> f =
                   SchemaBuilder.record("Source")
                           .namespace("io.debezium.connector.postgresql")
                           .fields()
                           .requiredString("version")
                           .requiredString("connector")
                           .requiredString("name")
                           .requiredLong("ts_ms")
                           .optionalString("snapshot")
                           .requiredString("db")
                           .optionalString("sequence")
                           .requiredString("schema")
                           .requiredString("table")
                           .optionalLong("txId")
                           .optionalLong("lsn")
                           .optionalLong("xmin");
           if (withTsUs) {
               f = f.optionalLong("ts_us");
           }
           return f.endRecord();
       }
   
       private static Schema envelope(Schema value, Schema source) {
           return SchemaBuilder.record("Envelope")
                   .namespace("pg.public.orders")
                   .fields()
                   .name("before").type().optional().type(value)
                   .name("after").type().optional().type(value)
                   .name("source").type(source).noDefault()
                   .requiredString("op")
                   .optionalLong("ts_ms")
                   .endRecord();
       }
   
       // ---------------------------------------------------------------- 
messages
   
       private byte[] insert(Schema env, int id, long lsn, boolean legacy, 
boolean tsUs)
               throws Exception {
           Schema valueSchema = 
env.getField("after").schema().getTypes().get(1);
           Schema sourceSchema = env.getField("source").schema();
           GenericRecord after = new GenericData.Record(valueSchema);
           after.put("id", id);
           after.put("name", "n" + id);
           if (legacy) {
               after.put("legacy", 7);
           }
           GenericRecord src = new GenericData.Record(sourceSchema);
           src.put("version", "2.7.0.Final");
           src.put("connector", "postgresql");
           src.put("name", "pg");
           src.put("ts_ms", 1_700_000_000_000L);
           src.put("snapshot", "false");
           src.put("db", "shop");
           src.put("sequence", "[\"" + (lsn - 8) + "\",\"" + lsn + "\"]");
           src.put("schema", "public");
           src.put("table", "orders");
           src.put("txId", 4242L);
           src.put("lsn", lsn);
           if (tsUs) {
               src.put("ts_us", 1_700_000_000_000_123L);
           }
           GenericRecord e = new GenericData.Record(env);
           e.put("after", after);
           e.put("source", src);
           e.put("op", "c");
           e.put("ts_ms", 1_700_000_000_500L);
   
           ByteArrayOutputStream out = new ByteArrayOutputStream();
           coder.writeSchema(env, out);
           BinaryEncoder enc = EncoderFactory.get().binaryEncoder(out, null);
           new GenericDatumWriter<GenericRecord>(env).write(e, enc);
           enc.flush();
           return out.toByteArray();
       }
   
       // ---------------------------------------------------------------- 
harness
   
       private DebeziumAvroDeserializationSchema 
deserializer(List<ReadableMetadata> metadata)
               throws Exception {
           DataType produced =
                   DataTypeUtils.appendRowFields(
                           PHYSICAL,
                           metadata.stream()
                                   .map(m -> DataTypes.FIELD(m.key, m.dataType))
                                   .collect(Collectors.toList()));
           RowType rowTypeDe =
                   
DebeziumAvroDeserializationSchema.createDebeziumAvroRowType(PHYSICAL, metadata);
           DebeziumAvroDeserializationSchema.MetadataConverter[] converters =
                   metadata.stream()
                           .map(
                                   m -> {
                                       int p =
                                               rowTypeDe
                                                       .getFieldNames()
                                                       
.indexOf(m.requiredAvroField.getName());
                                       return 
(DebeziumAvroDeserializationSchema.MetadataConverter)
                                               (row, pos) -> 
m.converter.convert(row, p);
                                   })
                           
.toArray(DebeziumAvroDeserializationSchema.MetadataConverter[]::new);
           // Same reader-schema construction as the production constructor.
           AvroRowDataDeserializationSchema typed =
                   new AvroRowDataDeserializationSchema(
                           new RegistryAvroDeserializationSchema<>(
                                   GenericRecord.class,
                                   
AvroSchemaConverter.convertToSchema(rowTypeDe, false),
                                   () -> coder),
                           
AvroToRowDataConverters.createRowConverter(rowTypeDe, false),
                           InternalTypeInfo.of(rowTypeDe));
           DebeziumAvroDeserializationSchema d =
                   new DebeziumAvroDeserializationSchema(
                           InternalTypeInfo.of(produced.getLogicalType()),
                           typed,
                           !metadata.isEmpty(),
                           converters,
                           rowTypeDe.getFieldNames().indexOf("source"),
                           () -> coder);
           d.open(new Ctx());
           return d;
       }
   
       private static List<RowData> run(DebeziumAvroDeserializationSchema d, 
byte[] msg)
               throws Exception {
           List<RowData> out = new ArrayList<>();
           d.deserialize(
                   msg,
                   new Collector<RowData>() {
                       @Override
                       public void collect(RowData r) {
                           out.add(r);
                       }
   
                       @Override
                       public void close() {}
                   });
           return out;
       }
   
       private static String prop(RowData row, int pos, String key) {
           MapData m = row.getMap(pos);
           StringData v = (StringData) ((GenericMapData) 
m).get(StringData.fromString(key));
           return v == null ? null : v.toString();
       }
   
       // ---------------------------------------------------------------- tests
   
       /** B: a later schema version adds a source field; does the map carry 
it? */
       @Test
       void laterSchemaVersionAddsSourceField() throws Exception {
           Schema v1 = envelope(value(false), source(false));
           Schema v2 = envelope(value(false), source(true));
           client.register(SUBJECT, new AvroSchema(v1));
           client.register(SUBJECT, new AvroSchema(v2));
           DebeziumAvroDeserializationSchema d =
                   
deserializer(Collections.singletonList(ReadableMetadata.SOURCE_PROPERTIES));
   
           run(d, insert(v1, 1, 1000L, false, false));
           List<RowData> rows = run(d, insert(v2, 2, 2000L, false, true));
   
           assertThat(prop(rows.get(0), 2, "lsn")).isEqualTo("2000");
           assertThat(prop(rows.get(0), 2, 
"ts_us")).isEqualTo("1700000000000123");
       }
   
       /** C control: no metadata; v2 drops a required column the DDL never 
declared. */
       @Test
       void droppedUndeclaredRequiredColumnWithoutMetadata() throws Exception {
           Schema v1 = envelope(value(true), source(false));
           Schema v2 = envelope(value(false), source(false));
           client.register(SUBJECT, new AvroSchema(v1));
           client.register(SUBJECT, new AvroSchema(v2));
           DebeziumAvroDeserializationSchema d = 
deserializer(Collections.emptyList());
   
           run(d, insert(v1, 1, 1000L, true, false));
           List<RowData> rows = run(d, insert(v2, 2, 2000L, false, false));
   
           assertThat(rows.get(0).getInt(0)).isEqualTo(2);
       }
   
       /** C: the same stream with source.properties declared. */
       @Test
       void droppedUndeclaredRequiredColumnWithMetadata() throws Exception {
           Schema v1 = envelope(value(true), source(false));
           Schema v2 = envelope(value(false), source(false));
           client.register(SUBJECT, new AvroSchema(v1));
           client.register(SUBJECT, new AvroSchema(v2));
           DebeziumAvroDeserializationSchema d =
                   
deserializer(Collections.singletonList(ReadableMetadata.SOURCE_PROPERTIES));
   
           run(d, insert(v1, 1, 1000L, true, false));
           List<RowData> rows = run(d, insert(v2, 2, 2000L, false, false));
   
           assertThat(rows.get(0).getInt(0)).isEqualTo(2);
           assertThat(prop(rows.get(0), 2, "lsn")).isEqualTo("2000");
       }
   
       private static class Ctx implements 
DeserializationSchema.InitializationContext {
           @Override
           public MetricGroup getMetricGroup() {
               return new UnregisteredMetricsGroup();
           }
   
           @Override
           public UserCodeClassLoader getUserCodeClassLoader() {
               return 
SimpleUserCodeClassLoader.create(getClass().getClassLoader());
           }
       }
   }
   
   ```
   
   </details>
   



-- 
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]

Reply via email to