[ 
https://issues.apache.org/jira/browse/FLINK-40262?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18100684#comment-18100684
 ] 

Dale Lane commented on FLINK-40262:
-----------------------------------

Thanks for the writeup. The scoping to converters-only makes sense. I've been 
reading it alongside the code and I've ended up questioning the Problem 
section, which I'd like to raise here, please.

Two things I'd like to ask for, and then my reasoning:
 * Could the Problem section be updated to name what actually produces the 
mismatch? (As far as I can see the registry subject doesn't, in either 
direction.)
 * Is the read half motivated by {{{}enum{}}}/{{{}fixed{}}}, or by the registry 
subject? If it's the latter, I think it could come out (which would take the 
eager-resolution path and the reader-schema navigation helpers with it, and 
leave a considerably smaller change)

 

*I don't think the registry subject can produce this*

Of the three sources listed, the registry subject is the one I'm bumping on.

{*}Reading{*}: with no {{avro-confluent.schema}} set, the reader schema is 
derived from the row type 
([RegistryAvroFormatFactory.java#L103-L106|https://github.com/apache/flink/blob/6ce85191d154c3691e0d514be770dfa9cc5f0c61/flink-formats/flink-avro-confluent-registry/src/main/java/org/apache/flink/formats/avro/registry/confluent/RegistryAvroFormatFactory.java#L103-L106]),
 and 
[RegistryAvroDeserializationSchema.java#L97-L103|https://github.com/apache/flink/blob/6ce85191d154c3691e0d514be770dfa9cc5f0c61/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/RegistryAvroDeserializationSchema.java#L97-L103]
 sets both the writer schema and the expected schema on the datum reader. So 
Avro resolves the registry's writer schema against the reader schema by name 
before the converter sees anything, and the record it hands over carries the 
_reader's_ schema and the reader's field order — meaning the {{record.get( i 
)}} at 
[AvroToRowDataConverters.java#L90|https://github.com/apache/flink/blob/6ce85191d154c3691e0d514be770dfa9cc5f0c61/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AvroToRowDataConverters.java#L90]
 is reading the column it thinks it is. (writer {{[b, a]}} into reader {{[a, 
b]}} will come back in reader order.)

Projection pushdown is the same point from another angle. 
{{createRuntimeDecoder}} projects the row type before deriving the schema 
([RegistryAvroFormatFactory.java#L101-L106|https://github.com/apache/flink/blob/6ce85191d154c3691e0d514be770dfa9cc5f0c61/flink-formats/flink-avro-confluent-registry/src/main/java/org/apache/flink/formats/avro/registry/confluent/RegistryAvroFormatFactory.java#L101-L106]),
 so under any pushdown the reader schema has fewer fields, at different 
positions, than whatever the registry is holding. If the converters were 
exposed to registry field order, projection pushdown would already be broken.

{*}Writing{*}: 
[ConfluentSchemaRegistryCoder.java#L85|https://github.com/apache/flink/blob/6ce85191d154c3691e0d514be770dfa9cc5f0c61/flink-formats/flink-avro-confluent-registry/src/main/java/org/apache/flink/formats/avro/registry/confluent/ConfluentSchemaRegistryCoder.java#L85]
 unconditionally registers whatever schema Flink hands it, which (in the 
absence of a {{avro-confluent.schema}} value) will be derived from the row 
type. There's no path that fetches an existing schema and conforms to it; 
{{register()}} and {{getById()}} are the only registry calls in the module. A 
reordered schema would just registered as a new version. Field order isn't part 
of Avro's resolution rules, so I'd expect a compatibility check to accept it.

That leaves the two constructors:
 * 
[AvroRowDataDeserializationSchema.java#L120-L127|https://github.com/apache/flink/blob/6ce85191d154c3691e0d514be770dfa9cc5f0c61/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AvroRowDataDeserializationSchema.java#L120-L127]
 * 
[AvroRowDataSerializationSchema.java#L96-L103|https://github.com/apache/flink/blob/6ce85191d154c3691e0d514be770dfa9cc5f0c61/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AvroRowDataSerializationSchema.java#L96-L103]

The caller supplies the nested schema and separately supplies a converter built 
from a {{{}RowType{}}}, with nothing tying the two together. On the write side, 
the supplied schema is what the converter is handed 
([AvroRowDataSerializationSchema.java#L119|https://github.com/apache/flink/blob/6ce85191d154c3691e0d514be770dfa9cc5f0c61/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AvroRowDataSerializationSchema.java#L119]),
 so where a caller does construct that pair the corruption is real, and I agree 
there's something here.

But inside Flink every caller either derives the schema from the row type or 
validates a supplied one first: {{{}RegistryAvroFormatFactory{}}}, both 
Debezium schemas (via {{validateSchemaString}} in their constructors), 
{{AvroFileFormatFactory}} in both directions, and the plain {{avro}} format has 
no schema option at all.

You mention connectors using those constructors. If that's where the real case 
comes from, naming one would make the ticket clearer than the registry subject 
does.

The third source, {{{}avro-confluent.schema{}}}, I'm not treating as one that 
goes away: the Scope section says relaxing [that 
check|https://github.com/apache/flink/blob/6ce85191d154c3691e0d514be770dfa9cc5f0c61/flink-formats/flink-avro-confluent-registry/src/main/java/org/apache/flink/formats/avro/registry/confluent/RegistryAvroFormatFactory.java#L255-L260]
 is part of the follow-up.

 

*Writing and reading look like different cases*

*Writing* is where I can most see this mattering. There's no reconciliation 
step on the way out, and since {{writeSchema}} always calls {{register()}} 
there's no fallback if the registry won't take the schema. Against a subject 
that's locked down, the job just fails. Someone pinned to a governed subject 
has no way to emit against that schema's field order today, and filling an 
unmatched Avro field from its declared default fits that well.

*Reading* I'm less sure about, because Avro resolution covers part of it 
already. 
[AvroSchemaConverter.java#L554-L556|https://github.com/apache/flink/blob/6ce85191d154c3691e0d514be770dfa9cc5f0c61/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/typeutils/AvroSchemaConverter.java#L554-L556]
 gives every nullable column a {{{}default: null{}}}, so a writer schema 
missing that field reads as null, and a {{NOT NULL}} column with no counterpart 
throws. This is the same pair of outcomes your {{AvroFieldMatcher}} implements. 
([AvroFileFormatFactory.java#L128-L130|https://github.com/apache/flink/blob/6ce85191d154c3691e0d514be770dfa9cc5f0c61/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AvroFileFormatFactory.java#L128-L130]
 already leans on that.)

Where I think reading does have a case is a supplied reader schema, for two 
reasons:
 * {{enum}} and {{{}fixed{}}}. The converters can already cope with both (the 
string converter emits an {{EnumSymbol}} (FLINK-39053) and the decimal 
converter accepts a {{GenericFixed)}} but the derived reader schema can't 
express either, and DECIMAL maps to {{bytes}} 
([AvroSchemaConverter.java#L530-L536|https://github.com/apache/flink/blob/6ce85191d154c3691e0d514be770dfa9cc5f0c61/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/typeutils/AvroSchemaConverter.java#L530-L536]),
 so resolution fails first: {{{}AvroTypeException: Found Color, expecting 
string{}}}, and {{{}Found Dec, expecting bytes{}}}. A hand-written 
{{avro-confluent.schema}} gets the enum case working today provided the field 
order matches the table (which is what the tests in 
[#27558|https://github.com/apache/flink/pull/27558] exercise) — so what NAME is 
adding is support for enum-or-fixed _and_ an order the table doesn't share.
 * Avro resolution is case-sensitive, and its alias handling matches reader 
aliases against writer field names, not against Flink column names. So your 
fuzzy stages do add something a derived reader schema can't get.

Both of those only apply where the reader schema is supplied, though, which 
lands back at the same two sources rather than at the registry subject.

Apologies if I've misread any of this, I might've gone down a rabbit-hole 
thinking this through!

> Support matching Avro record fields by name in the Avro RowData converters
> --------------------------------------------------------------------------
>
>                 Key: FLINK-40262
>                 URL: https://issues.apache.org/jira/browse/FLINK-40262
>             Project: Flink
>          Issue Type: Improvement
>          Components: Formats (JSON, Avro, Parquet, ORC, SequenceFile)
>            Reporter: Pritam Kumar
>            Assignee: Pritam Kumar
>            Priority: Major
>              Labels: pull-request-available
>
> h2. Problem
>   {{RowDataToAvroConverters}} and {{AvroToRowDataConverters}} pair a 
> {{RowType}} field with an Avro record field by *ordinal position*:
>   {code:java}
>   for (int i = 0; i < length; ++i) {
>       final Schema.Field schemaField = fields.get(i);
>       record.put(i, fieldConverters[i].convert(
>               schemaField.schema(), fieldGetters[i].getFieldOrNull(row)));
>   }
>   {code}
>   * a schema registry subject;
>   * the {{avro-confluent.schema}} option;
>   * the {{AvroRowDataSerializationSchema(rowType, nestedSchema, converter)}} 
> and {{AvroRowDataDeserializationSchema(nestedSchema, converter, typeInfo)}} 
> constructors, which downstream connectors use directly.
>   In those cases column {{i}} and Avro field {{i}} need not be the same 
> field. Writing puts each value into whichever field happens to sit at that 
> position, and reading does the mirror image. Where the types at a position 
> differ the job fails
>   with a {{ClassCastException}}; where they happen to agree, the data is 
> silently corrupted.
>   h3. Example
>   Table {{a STRING, b STRING}} against a registry schema that declares {{b}} 
> before {{a}}. Both are strings, so nothing fails: {{a}}'s value is written 
> into {{b}} and vice versa.
>   h2. Proposal
>   Add an explicit strategy and thread it through both converters:
>   {code:java}
>   public enum FieldMatching { INDEX, NAME }
>   {code}
>   * {{INDEX}} — today's behaviour, and the default. Unchanged.
>   * {{NAME}} — pair fields by name. Names are compared exactly first, then 
> against Avro field {{aliases}}, and finally ignoring case ({{Locale.ROOT}}). 
> Each stage runs to completion before the next, so a fuzzy match can never 
> claim an Avro field
>   that another column matches exactly.
>   Every existing overload is kept and delegates with {{INDEX}}, so the change 
> is binary compatible (japicmp clean).
>   h3. What NAME refuses
>   Name matching should not paper over a schema that does not line up. 
> Resolution fails, naming the offending field, when:
>   * a column matches more than one Avro field;
>   * two columns match the same Avro field;
>   * a column has no Avro counterpart and we are writing — the column would be 
> dropped;
>   * a {{NOT NULL}} column has no Avro counterpart and we are reading — it 
> could only be read as {{NULL}};
>   * an Avro field that no column maps to is neither nullable nor declares a 
> default — the record cannot be encoded at all, and Avro surfaces that as a 
> bare {{NullPointerException}}.
>   Tolerated:
>   * an Avro field that no column maps to but that declares a default is 
> written with that default;
>   * an Avro field that no column reads is ignored, which is ordinary 
> projection.
>   h3. Cost
>   Resolution runs once per (row type, schema) pair and is memoized, so 
> {{NAME}} costs one array lookup per field per record: no hashing, no 
> lowercasing, no allocation on the hot path. The memo is {{transient}} and 
> rebuilt after the converter is
>   shipped to a task.
>   h2. Scope
>   This issue covers the converters only, which makes the capability reachable 
> from Java but not from SQL.
>   Exposing the strategy as an {{avro-confluent.field-matching}} option is 
> deliberately left to a follow-up so that the two can be reviewed 
> independently. That part also has to relax the order sensitive schema check in
>   {{RegistryAvroFormatFactory#getAvroSchema}}, which compares the converted 
> {{DataType}} with {{equals()}} and therefore rejects a reordered schema 
> during planning with _"Schema provided for 'avro-confluent' format does not 
> match the table
>   schema"_ — so today such a schema never reaches the converters at all. 
> Happy to fold that into this issue instead if reviewers would rather have it 
> in one go.
>   The plain {{avro}} format needs no option: it always derives its schema 
> from the table, so field order agrees by construction.
>   h2. Compatibility
>   {{INDEX}} is the default everywhere, no existing signature or default value 
> changes, and japicmp reports no incompatibility.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to