-
Notifications
You must be signed in to change notification settings - Fork 1k
Implement arrow-avro SchemaStore and Fingerprinting To Enable Schema Resolution #8006
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
b549452
to
a4a4df8
Compare
a4a4df8
to
ca39cba
Compare
… and made the schema module public. Integrated new `SchemaStore` to the `Decoder` in `reader/mod.rs`. Stubbed out `AvroField::resolve_from_writer_and_reader` in `codec.rs`. Added new tests to cover changes
ca39cba
to
4890faa
Compare
@veronica-m-ef I wonder if you might have some time to help review this PR, as you previously contributed to this code? |
Perhaps @svencowart you might also be interested and able to help review this PR? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is definitely a big piece of work, but I don't know how to split up the functionality of this PR -- except some of the cosmetic changes, code movement, and variable renames should ideally be eliminated or moved to a different PR for clarity.
arrow-avro/src/codec.rs
Outdated
writer: &'a Schema<'a>, | ||
reader: &'a Schema<'a>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aside: I guess this is a low-level avro schema instance, not the arrow schema Schema
?
At least, I don't remember arrow Schema objects having a lifetime parameter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah -- SchemaRef
is from arrow-schema, but Schema
is crate-local.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we could rename to disambiguate? ArrowSchemaRef
vs. [Avro]Schema
?
arrow-avro/src/schema.rs
Outdated
/// <https://avro.apache.org/docs/1.11.1/specification/#parsing-canonical-form-for-schemas> | ||
#[inline] | ||
pub fn generate_canonical_form(schema: &Schema) -> String { | ||
serde_json::to_string(&parse_canonical_json(schema)).unwrap() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unwrap
because the to_string
call can never fail for some reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cleaned this up in my last commit. Ty for catching this.
arrow-avro/src/schema.rs
Outdated
let canonical = generate_canonical_form(schema); | ||
match hash_type { | ||
FingerprintAlgorithm::Rabin => Fingerprint::Rabin(compute_fingerprint_rabin(&canonical)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Converting something to a string just so we can hash it seems really expensive... but if I understand correctly, the avro spec mandates it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct unfortunately. The fingerprints are supposed to be of a Schema
in canonical form
Luckily, there shouldn't be a scenario where we need to parse a schema and fingerprint it while decoding.
arrow-avro/src/schema.rs
Outdated
/// The hashing algorithm used for generating fingerprints. | ||
fingerprint_algorithm: FingerprintAlgorithm, | ||
/// A map from a schema's fingerprint to the schema itself. | ||
schemas: HashMap<Fingerprint, Schema<'a>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This map probing seems vulnerable to hash collisions, because we probe only by hash?
(as opposed to passing the schema, probing by hash, and then confirming against the schema)?
From the spec:
fingerprints are not meant to provide any security guarantees, even the longer SHA-256-based ones. Most Avro applications should be surrounded by security measures that prevent attackers from writing random data and otherwise interfering with the consumers of schemas. We recommend that these surrounding mechanisms be used to prevent collision and pre-image attacks (i.e., “forgery”) on schema fingerprints, rather than relying on the security properties of the fingerprints themselves.
Granted, the chances of a collision should be vanishingly small for a reasonable number of schemas and a uniformly distributed 64-bit hash, so maybe we don't care?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was planning to add some improvements to this logic when I got back in here for the extra hash types. However I went ahead and added a check to the register
function. It was pretty trivial and was worth it.
arrow-avro/src/schema.rs
Outdated
/// | ||
/// An `Option` containing a clone of the `Schema` if found, otherwise `None`. | ||
pub fn lookup(&self, fp: &Fingerprint) -> Option<Schema<'a>> { | ||
self.schemas.get(fp).cloned() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's an expensive clone (for a big schema)... should we return a reference to the schema instead, and let the caller clone it if they wish?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a great suggestion, ty for making it. This was included in my last commit as well.
self | ||
} | ||
|
||
fn build_impl<R: BufRead>(self, reader: &mut R) -> Result<(Header, Decoder), ArrowError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these methods deleted? Or moved? Or just github is giving a messy diff?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These methods got deleted. I was able to confirm that only the Reader
would ever expect a Header
and was able to change build
and build_decoder
to this:
/// Create a [`Reader`] from this builder and a `BufRead`
pub fn build<R: BufRead>(self, mut reader: R) -> Result<Reader<R>, ArrowError> {
self.validate()?;
let header = read_header(&mut reader)?;
let decoder = self.make_decoder(Some(&header))?;
Ok(Reader {
reader,
header,
decoder,
block_decoder: BlockDecoder::default(),
block_data: Vec::new(),
block_cursor: 0,
finished: false,
})
}
/// Create a [`Decoder`] from this builder.
pub fn build_decoder(self) -> Result<Decoder, ArrowError> {
self.validate()?;
self.make_decoder(None)
}
/// | ||
/// When enabled, string data from Avro files will be loaded into | ||
/// Arrow's StringViewArray instead of the standard StringArray. | ||
pub fn with_utf8_view(mut self, utf8_view: bool) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There seems to be considerable code movement in this part of the file... makes it hard to see what meaningfully changed. Is there a way to clean up the diff?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
100% I'm working on that now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried to clean up the diff with my latest pushes. Let me know if that's better and easier to follow.
arrow-avro/src/reader/mod.rs
Outdated
// No initial fingerprint; the first record must contain one. | ||
// A temporary decoder is created from the reader schema. | ||
_ => { | ||
let dec = self.make_record_decoder(&reader_schema, None)?; | ||
(None, dec) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks error-prone... but I guess there's no way to avoid it if the spec allows this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more of the default state that we'd need to cover. What we could do is if the schema_store
is set without an active_fingerprint
, then throw an explicit error in the Decoder
that is more clear than ArrowError::ParseError(format!("Unknown fingerprint: {new_fingerprint:?}"))
.
I'll clean that up in the morning, this is a good call out!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this check + early failure to the beginning of Decoder::decoder
to help clean this up a bit:
if self.active_fingerprint.is_none()
&& self.writer_schema_store.is_some()
&& !data.starts_with(&SINGLE_OBJECT_MAGIC)
{
return Err(ArrowError::ParseError(
"Expected single‑object encoding fingerprint prefix for first message \
(writer_schema_store is set but active_fingerprint is None)"
.into(),
));
}
Let me know what you think.
arrow-avro/src/reader/mod.rs
Outdated
// A temporary decoder is created from the reader schema. | ||
_ => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it valid to ignore the case where we have Some(fp)
but no schema store? That seems like an error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
100% It's an error, I'm just doing that check at the start, in the validate
method:
(None, _, Some(_), _) => Err(ArrowError::ParseError(
"Active fingerprint requires a writer schema store".into(),
)),
arrow-avro/src/reader/mod.rs
Outdated
Ok(Decoder { | ||
batch_size: self.batch_size, | ||
decoded_rows: 0, | ||
active_fp: init_fp, | ||
active_decoder: initial_decoder, | ||
cache: HashMap::new(), | ||
lru: VecDeque::new(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two constructor calls seem to have a lot of redundancy. Would it be worthwhile to factor out the args that actually differ, and create the decoder only once, outside the match?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was a good call out. I included this abstraction in my latest commit.
Co-authored-by: Ryan Johnson <[email protected]>
Co-authored-by: Ryan Johnson <[email protected]>
Co-authored-by: Ryan Johnson <[email protected]>
9dde02c
to
da7b1b9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heading out the door for a couple days, but this refresh looks way better at a glance.
Will hopefully get a more thorough pass on Wed
arrow-avro/src/reader/mod.rs
Outdated
while self.cache.len() > self.max_cache_size { | ||
if let Some(lru_key) = self.cache.keys().next().cloned() { | ||
self.cache.shift_remove(&lru_key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will pay quadratic work for a cache with a lot of extra entries. Hopefully that's a rare case tho?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually... looking at the code, there is only one call site for this method, and there will be at most one extra entry to remove. We should probably just bake that in at the call site, instead of splitting the logic up like this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed this change up in my latest commit. That was a good catch.
Co-authored-by: Ryan Johnson <[email protected]>
Co-authored-by: Ryan Johnson <[email protected]>
0608fd1
to
98ae29a
Compare
98ae29a
to
25c3899
Compare
@scovich Really appreciate the solid review on a bigger PR like this. I got those changes pushed up and the code is definitely looking much better. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made a full pass now. Definitely headed a good direction.
arrow-avro/src/reader/mod.rs
Outdated
// A batch is complete when its `remaining_capacity` is 0. It may be completed early if | ||
// a schema change is detected or there are insufficient bytes to read the next prefix. | ||
// A schema change requires a new batch. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment seems a bit misplaced? Should it be at L193 below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tho re-reading, comment seems to talk about both locations so it probably won't fit well in either one. Maybe it should be at L185 and explain the loop as a whole?
arrow-avro/src/reader/mod.rs
Outdated
// Forcing the batch to be full ensures `flush` is called next. | ||
if self.decoded_rows > 0 { | ||
self.decoded_rows = self.batch_size; | ||
if self.remaining_capacity < self.batch_size { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting.. it's possible for two schema changes to come with no rows in between?
And this check prevents emitting an empty batch in that corner case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@scovich That was the intention. Not sure how common this scenario is in the real world, however with single object encoding you can legally receive two different schema fingerprints back to back before any rows are decoded.
Also I'll update the comment on L254 to better the reflect the changes.
arrow-avro/src/reader/mod.rs
Outdated
initial_decoder, | ||
init_fingerprint, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems better to pick one or the other of init_
vs. initial_
? (slight preference toward the latter)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bump? we still have both initial_ and init_ here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh, I thought I had changed that. Sorry I was tired last night. I'll make sure that's initial_fingerprint
in my next push.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok this is resolved. Can confirm the init_
is gone in my latest push. Sorry about making you have to catch that twice.
arrow-avro/src/reader/mod.rs
Outdated
(Some(_), _, None, true) => Err(ArrowError::ParseError( | ||
"static_store_mode=true requires an active fingerprint".into(), | ||
)), | ||
_ => Ok(()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of defaulting to Ok
, it seems better to enumerate the valid cases and default (if necessary) to a generic Err
?
On a related note -- have we done the full "truth table" for these values, to determine which combos are definitely valid vs. definitely invalid? Otherwise I worry we might overlook some invalid or ambiguous cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed up those improvements and included a truth table in the comments. Really good idea.
Also I removed the self.validate()?;
call from the ReaderBuilder::build
method since it's only really needed in ReaderBuilder::build_decoder
arrow-avro/src/schema.rs
Outdated
Some(ns) if !name.contains('.') => format!("{ns}.{name}"), | ||
_ => name.to_string(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the rules for formatting complex field names in avro? For example, is a.`b.c`.d
(a three-deep path) allowed? What about a."hi".b
? etc.
Asking because the default match arm seems a bit questionable, because it implicitly covers Some(ns) if name.contains('.')
but then ignores ns
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the rules for formatting complex field names in avro?
- Avro does not support “complex” JSON‑dot or quoted paths inside a single name.
- Every identifier (whether it is a name on a record/enum/fixed type, a field name, or an enum symbol) must match the regex
^[A‑Za‑z_][A‑Za‑z0‑9_]*$
. - the only place the period
.
is between such identifiers when building a namespace‑qualified full name. When the name attribute already contains a.
, Avro treats that string as the full name and the separate namespace attribute (if present) must be ignored.
For example, is a.
b.c
.d (a three-deep path) allowed? What about a."hi".b? etc.
a."hi".b
anda.`b.c`.d
are invalid because of the quotes / backticks.a.b.c.d
is valid when used as the name of a record/enum/fixed type, where it is parsed as namespace:a.b.c
, name:d
.
Asking because the default match arm seems a bit questionable, because it implicitly covers Some(ns) if name.contains('.') but then ignores ns?
Basically once a name
contains a dot it is already the fullname, and namespace
must be ignored. Not sure exactly why Avro has two different ways to express a fullname, but they are both valid and should be handled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, thanks for the explanation of yet another intricacy of the spec.
Maybe worth a code comment summarizing this?
arrow-avro/src/reader/mod.rs
Outdated
let new_decoder = if let Some(decoder) = self.cache.shift_remove(&new_fingerprint) { | ||
decoder | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let new_decoder = if let Some(decoder) = self.cache.shift_remove(&new_fingerprint) { | |
decoder | |
} else { | |
let new_decoder = self.cache.shift_remove(&new_fingerprint).unwrap_or_else(|| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh... all those ?
complicate things a lot. Never mind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, what if this helper method only dealt with the decoder, and the (single) caller installed it? Instead of:
self.prepare_schema_switch(new_fp)?;
do
let new_decoder = match self.cache.shift_remove(&new_fingerprint) {
Some(decoder) => decoder,
None => self.create_decoder_for(new_fingerprint)?,
};
self.pending_schema = Some((new_fingerprint, new_decoder))
Where create_decoder_for
is the logic from this else
block?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super clean approach! I made those changes.
arrow-avro/src/reader/mod.rs
Outdated
(None, _, Some(_)) => Err(ArrowError::ParseError( | ||
"Active fingerprint requires a writer schema store".into(), | ||
)), | ||
_ => Ok(()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we certain all other combos are valid? Perhaps better to enumerate the known-good and known-bad cases instead? Seems like we shouldn't even need a default match arm that that point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going with your enumeration suggestion. That will be much cleaner and maintainable. Ty for for calling that out.
let mut fp = i as u64; | ||
let mut j = 0; | ||
while j < 8 { | ||
fp = (fp >> 1) ^ (EMPTY & (0u64.wrapping_sub(fp & 1))); | ||
j += 1; | ||
} | ||
fp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we can't use iterators in const
context?
Otherwise 0..8
would be helpful here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still doesn't seem to be stable yet: rust-lang/rust#87575
This would be nice though:
const fn one_entry(i: usize) -> u64 {
let mut fp = i as u64;
for _ in 0..8 {
fp = (fp >> 1) ^ (EMPTY & (0u64.wrapping_sub(fp & 1)));
}
fp
}
I'll add comments with those details.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah... if iterator machinery were all const, this whole function would just be a fold
:
(0..8).fold(i as u64, |fp, _| (fp >> 1) ^ (EMPTY & (0u64.wrapping_sub(fp & 1))))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still doesn't seem to be stable yet: rust-lang/rust#87575
Wow, that led down a fascinating rabbit hole, ending at
https://github.com/oli-obk/rfcs/blob/const-trait-impl/text/0000-const-trait-impls.md
I'll be interested to see if/when that ever lands!
Co-authored-by: Ryan Johnson <[email protected]>
98f0b91
to
4f734e2
Compare
Co-authored-by: Ryan Johnson <[email protected]>
4924637
to
9c828c6
Compare
I know this PR has a lot of comments and quite a few refinements were needed. I also know it was way too big. To move forward, would it be best to simply focus on getting the I do apologize for both the size of this PR and the quality issues. I'll do better going forward. Thank you again @scovich for the amount of time and effort you've put into reviewing this. Let me know what I should do and I'll get on it immediately. |
Honestly, I think the "state machine" for avro decoding is just really complex. It felt like a lot of the questions and churn ultimately come from that underlying complexity. And my lack of familiarity with the avro spec. Not sure how easily those issues could be avoided merely by raising a smaller PR?
Interesting. By "schema.rs changes" you mean adding the new
This is NOT the kind of run of the mill logic that I would normally associate with "quality issues."
Now that we know more, the split you propose seems quite reasonable; I'm not sure it was obvious two weeks ago tho? |
Thats correct and that's my understanding as well.
I really appreciate that! I just thought I should have caught some of that subtlety upfront.
It crossed my mind honestly. I just thought that:
I'll get that PR up right now and link it here. |
6bec449
to
78b6633
Compare
…g in arrow-avro reader.
78b6633
to
dc56c70
Compare
# Which issue does this PR close? - Part of #4886 - Pre-work for #8006 # Rationale for this change Apache Avro’s [single object encoding](https://avro.apache.org/docs/1.11.1/specification/#single-object-encoding) prefixes every record with the marker `0xC3 0x01` followed by a `Rabin` [schema fingerprint ](https://avro.apache.org/docs/1.11.1/specification/#schema-fingerprints) so that readers can identify the correct writer schema without carrying the full definition in each message. While the current `arrow‑avro` implementation can read container files, it cannot ingest these framed messages or handle streams where the writer schema changes over time. The Avro specification recommends computing a 64‑bit CRC‑64‑AVRO (Rabin) hashed fingerprint of the [parsed canonical form of a schema](https://avro.apache.org/docs/1.11.1/specification/#parsing-canonical-form-for-schemas) to look up the `Schema` from a local schema store or registry. This PR introduces **`SchemaStore`** and **fingerprinting** to enable: * **Zero‑copy schema identification** for decoding streaming Avro messages published in single‑object format (i.e. Kafka, Pulsar, etc) into Arrow. * **Dynamic schema evolution** by laying the foundation to resolve writer reader schema differences on the fly. **NOTE:** Integration with `Decoder` and `Reader` coming in next PR. # What changes are included in this PR? | Area | Highlights | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`schema.rs`** | *New* `Fingerprint`, `SchemaStore`, and `SINGLE_OBJECT_MAGIC`; canonical‑form generator; Rabin fingerprint calculator; `compare_schemas` helper. | | **`lib.rs`** | `mod schema` is now `pub` | | **Unit tests** | New tests covering fingerprint generation, store registration/lookup, unknown‑fingerprint errors, and interaction with UTF8‑view decoding. | | **Docs & Examples** | Extensive inline docs with examples on all new public methods / structs. | # Are these changes tested? Yes. New tests cover: 1. **Fingerprinting** against the canonical examples from the Avro spec 2. **`SchemaStore` behavior** deduplication, duplicate registration, and lookup. # Are there any user-facing changes? N/A
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good enough for now, I think.
There are lots of follow-ups to simplify the logic, especially places where we funnel two distinct cases through one code path that has to tease them back apart. But I think there's ongoing work to address those?
100% I have that code mostly ready. Should only take me a few hours to get that follow-up PR up. |
Co-authored-by: Ryan Johnson <[email protected]>
Co-authored-by: Ryan Johnson <[email protected]>
Co-authored-by: Ryan Johnson <[email protected]>
6724d75
to
ef9051e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @jecsand838 and @scovich for the review -- very much apprecaited
I skimmed this PR and it (as the other PRs from @jecsand838 ) looks well commented and tested (and therefore maintainable). I am relying on @scovich 's work for the detailed review.
Thank you
71f63c7
to
5edc763
Compare
…ation and schema fingerprinting.
5edc763
to
c450401
Compare
🚀 |
# Which issue does this PR close? - Part of #4886 - Extends work initiated in #8006 # Rationale for this change This introduces support for Confluent schema registry ID handling in the arrow-avro crate, adding compatibility with Confluent's wire format. These improvements enable streaming Apache Kafka, Redpanda, and Pulsar messages with Avro schemas directly into arrow-rs. # What changes are included in this PR? - Adds Confluent support - Adds initial support for SHA256 and MD5 algorithm types. Rabin remains the default. # Are these changes tested? Yes, existing tests are all passing, and tests for ID handling have been added. Benchmark results show no appreciable changes. # Are there any user-facing changes? - Confluent users need to provide the ID fingerprint when using the `set` method, unlike the `register` method which generates it from the schema on the fly. Existing API behavior has been maintained. - SchemaStore TryFrom now accepts a `&HashMap<Fingerprint, AvroSchema>`, rather than a `&[AvroSchema]` Huge shout out to @jecsand838 for his collaboration on this! --------- Co-authored-by: Connor Sanders <[email protected]>
Which issue does this PR close?
Part of Add Avro Support #4886
Follow up to Implement arrow-avro Reader and ReaderBuilder #7834
Rationale for this change
Apache Avro’s single object encoding prefixes every record with the marker
0xC3 0x01
followed by aRabin
schema fingerprint so that readers can identify the correct writer schema without carrying the full definition in each message.While the current
arrow‑avro
implementation can read container files, it cannot ingest these framed messages or handle streams where the writer schema changes over time.The Avro specification recommends computing a 64‑bit CRC‑64‑AVRO (Rabin) hashed fingerprint of the parsed canonical form of a schema to look up the
Schema
from a local schema store or registry.This PR introduces
SchemaStore
and fingerprinting to enable:NOTE: Schema Resolution support in
Codec
andRecordDecoder
coming the next PR.What changes are included in this PR?
reader/mod.rs
C3 01
prefix, extracts the fingerprint, looks up the writer schema in aSchemaStore
, and switches to an LRU cachedRecordDecoder
without interrupting streaming; supportsstatic_store_mode
to skip the 2 byte peek for high‑throughput fixed‑schema pipelines.ReaderBuilder
.with_writer_schema_store
,.with_active_fingerprint
,.with_static_store_mode
,.with_reader_schema
,.with_max_decoder_cache_size
, with rigorous validation to prevent misconfiguration.Are these changes tested?
Yes. New tests cover:
SchemaStore
behavior deduplication, duplicate registration, and lookup.static_store_mode=true
, ensuring the prefix is treated as payload, the 2 byte peek is skipped, and no schema switch is attempted.Are there any user-facing changes?
N/A
Follow-Up PRs
Fingerprint
variant onSchemaStore
for Confluent Schema Registry compatibility