Skip to content

avro: field aliases as a resolution rung — measured, and unsound as a default (crossed-alias counterexample) #104

Open

Description

@kryptt

Schema.Field.aliases() is Avro's own field-rename mechanism, and eo's main sources never read it. It looks like the obvious next rung after #98: an alias-aware nominalIndex resolves the aliased rename+swap that #98's residual documents as unreachable.

It was implemented and measured, and it is not safe as a default. Filing so the counterexample is on record before someone re-proposes it.

What works

Patching nominalIndex (AvroWalk.scala:541) to consult field.aliases() after the exact-name and normalised-name attempts does resolve the aliased residual — the case where a schema renames occurredAt→ts and seqNo→sequence, swaps their positions, and records the old names as aliases.

Why it is unsound as a default

An alias names a historical wire name, not a Scala field name. The two can be anti-correlated by an ordinary migration. The counterexample is a mislabel correction — someone discovers two columns were named backwards and fixes it, recording the old (wrong) names as aliases:

final case class P2Fix(a: Long, b: Long)

given Codec[P2Fix] = Codec.record[P2Fix]("Fix", "eo.alias") { f =>
  (
    f("a_fixed", _.a, aliases = Seq("b")),   // was called "b", wrongly
    f("b_fixed", _.b, aliases = Seq("a")),   // was called "a", wrongly
  ).mapN((x, y) => P2Fix(x, y))
}

Case field a matches schema field b_fixed by alias and a_fixed by position and by normalised name. The alias rung believes the alias and mis-targets. Measured on the patched build: .field(_.a) returns Some(22) where the truth is 11.

That is a case #98 gets right today, silently broken by the new rung — the exact failure class #98 exists to eliminate.

All 212 avro tests stayed green with and without the patch, because no fixture covers crossed aliases. Any future attempt needs this as a pinned negative fixture first.

If it is ever revisited

Under the same all-or-nothing discipline #98 established: consult aliases only when the alias map and the name/position map agree, and abstain whenever they disagree. Disagreement is exactly the mislabel-correction signal, and abstaining there costs nothing — position already handles it.

Worth noting the alias rung is strictly dominated by the differential codec probe (#100), which resolves both the aliased and the un-aliased rename+swap without consulting schema history at all, and scored 7 true positives / 0 false positives / 1 false negative on the full DivergentCodecs + ResolutionResidualSpec matrix.

Related: #99 (targeting laws), #100 (differential probe), #103 (the rung's complexity).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions