Introduce invalid data error macro - #2928
xanderbailey wants to merge 5 commits into
Conversation
7a4473f to
b4c444d
Compare
|
Not sure what folks think about this change but I personally think it improves readability. |
| /// // Attaching a source error | ||
| /// let n: i32 = s.parse().map_err(|e| invalid_data!("not an int: {s}").with_source(e))?; | ||
| /// ``` | ||
| macro_rules! invalid_data { |
There was a problem hiding this comment.
Just one design thought: the expr arm accepts any expression, so if somone writes invalid_data!(format!(...)) (out of habit), we will double allocate. Not worth guarding against though.
|
This pull request has been marked as stale due to 30 days of inactivity. It will be closed in 1 week if no further activity occurs. If you think that’s incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the dev@iceberg.apache.org list. Thank you for your contributions. |
|
@blackmwk just wanted to tag you for thoughts before this goes stale, if we're happy then I'll resolve conflicts |
laskoviymishka
left a comment
There was a problem hiding this comment.
agreed with anoopj, this is a nice cleanup — the invalid_data! macro reads well and the call sites are genuinely quieter for it.
Since it's a script-driven rewrite across ~68 files, I did a second pass focused on one thing: whether the mechanical migration preserved the old error semantics 1:1 everywhere. Mostly yes, but a couple of things I'd want to sort before merge.
The one real behavior change is in avro/schema.rs. The old code passed "...iceberg schema: {avro_schema}" as a plain &str, so the braces printed verbatim — a latent bug. The macro's literal arm wraps it in format!, so now it actually interpolates avro_schema. That's a strictly better message, but it's a silent semantic change riding along on a "no behavior change" cleanup, and it only compiles because AvroSchema: Display happens to hold. I'd make it deliberate — call it out in the description (or switch to {avro_schema:?}) so it's on the record rather than an accident.
The other thing is scope: delete_vector.rs still has ~9 Error::new(ErrorKind::DataInvalid, ...) sites and arrow/reader/pipeline.rs has at least one, none of which are in this PR. A partial migration leaves the crate with two idioms for the same error and no way to tell "skipped on purpose" from "missed." I'd either run the script over those too, or add a line to the description saying why they're excluded.
Everything else is nits — a handful of leftover .to_string() calls the literal arm makes redundant, one invalid_data!(e.to_string()) that drops the source chain, and a couple of macro-hygiene things I left inline.
Once the avro change is intentional and the migration scope is settled, happy to take another pass and approve.
| ErrorKind::DataInvalid, | ||
| "Can't convert non record avro schema to iceberg schema: {avro_schema}", | ||
| Err(invalid_data!( | ||
| "Can't convert non record avro schema to iceberg schema: {avro_schema}" |
There was a problem hiding this comment.
This is the one spot where the rewrite changes behavior. Before, {avro_schema} sat inside a plain &str, so the braces printed verbatim and the substitution never happened — a latent bug. The macro's literal arm wraps the string in format!, so now it actually interpolates avro_schema via Display.
It's a better message, and it only compiles because AvroSchema: Display holds, so nothing's broken. But it's a silent semantic change on a cleanup that's meant to be behavior-preserving. I'd make it deliberate — note it in the PR description, or switch to {avro_schema:?} — so it reads as an intentional fix rather than an accident.
| ) | ||
| .with_context("value", value) | ||
| .with_source(e) | ||
| invalid_data!("Failed to parse field id".to_string()) |
There was a problem hiding this comment.
The .to_string() is redundant here — the literal arm already runs the message through format!, so invalid_data!("Failed to parse field id") produces the exact same allocation. Dropping the suffix keeps it on the more idiomatic literal arm.
Same shape in about six other spots: arrow/schema.rs (the field-id and decimal-type branches), spec/manifest/mod.rs (both the serialize and deserialize sites), spec/values/datum.rs (the AboveMax/BelowMin arm), and spec/schema/prune_columns.rs.
| /// let n: i32 = s.parse().map_err(|e| invalid_data!("not an int: {s}").with_source(e))?; | ||
| /// ``` | ||
| macro_rules! invalid_data { | ||
| ($fmt: literal $(, $($arg:tt)*)?) => { |
There was a problem hiding this comment.
One refinement on the literal arm: invalid_data!("static message") expands to format!("static message"), which clippy's useless_format can flag — and with -D warnings in the Makefile that could bite CI depending on toolchain. Worth confirming CI is green here, since clippy's firing span through macros varies by version.
If it does fire, a dedicated no-arg arm sidesteps it and doubles as the canonical form for the bare-literal sites:
($fmt: literal) => {
$crate::error::Error::new($crate::error::ErrorKind::DataInvalid, $fmt)
};
($fmt: literal, $($arg:tt)*) => {
$crate::error::Error::new($crate::error::ErrorKind::DataInvalid, format!($fmt, $($arg)*))
};
($msg: expr $(,)?) => { /* unchanged */ };That way invalid_data!("...") skips format! entirely, and the leftover .to_string() calls just become bare literals. wdyt?
| (PrimitiveType::Fixed(_), PrimitiveLiteral::Binary(value)) => { | ||
| let array = FixedSizeBinaryArray::try_from_iter(std::iter::once(value.as_slice())) | ||
| .map_err(|e| Error::new(ErrorKind::DataInvalid, e.to_string()))?; | ||
| .map_err(|e| invalid_data!(e.to_string()))?; |
There was a problem hiding this comment.
invalid_data!(e.to_string()) stringifies the source and drops the structured chain, so err.source() comes back None. Not a regression — the old code did the same — but since we're on the line, invalid_data!("{e}").with_source(e) keeps the chain and matches the many sites that already do .with_source. Same story for the other e.to_string() spots that don't chain the source.
| } | ||
|
|
||
| // Crate-internal macro: re-exported so other modules can `use crate::error::invalid_data;`. | ||
| pub(crate) use invalid_data; |
There was a problem hiding this comment.
Small thing while we're here: ensure_data_valid! just above is #[macro_export] but invalid_data! is pub(crate). That's a defensible split, but nothing says so, and a future contributor could #[macro_export] this without realizing it'd leak into the public API. A one-line comment on the intent — plus a note in the doc example that the use crate::error::invalid_data path is crate-internal — would lock it in.
| } | ||
|
|
||
| #[test] | ||
| fn test_invalid_data_macro() { |
There was a problem hiding this comment.
The test covers the literal arm nicely but not the expr arm directly — it's only exercised via the migrated call sites compiling. A quick let s = String::from("computed"); assert_eq!(invalid_data!(s).message(), "computed"); would pin it down.
| } | ||
|
|
||
| use super::DataFileFormat; | ||
| use crate::error::invalid_data; |
There was a problem hiding this comment.
This use crate::error::invalid_data; landed mid-file next to the static STATUS block rather than in the top import group. Worth moving up with the other use crate:: lines.
b4c444d to
7b8eed7
Compare
7b8eed7 to
5286e7e
Compare
laskoviymishka
left a comment
There was a problem hiding this comment.
Nice — this is close now. The scope ask from last round landed: delete_vector.rs and arrow/reader/pipeline.rs are both migrated in this pass, so the crate isn't split across two idioms for the same error anymore. The leftover .to_string() calls and the with_source chains are all clean too, and the macro hygiene ($crate:: paths, no #[macro_export]) reads well.
One thing carried over: the avro/schema.rs interpolation is still riding along silently. I'm not asking to revert it — it's a strictly better message — just to make it deliberate, either a note in the description or {avro_schema:?}, so it's on the record. That was my one gate from last round.
The one new thing is the literal arm always going through format!, which is what clippy::useless_format keys on for the ~30 zero-arg call sites. It may or may not surface from inside the expansion, so really I just want to confirm clippy is green on this PR — if it is, the single-arm design is fine as-is. I left that inline along with a tiny .with_source duplication nit in arrow/schema.rs.
Confirm clippy's green and make the avro change intentional, and I'm happy to approve.
| ErrorKind::DataInvalid, | ||
| "Can't convert non record avro schema to iceberg schema: {avro_schema}", | ||
| Err(invalid_data!( | ||
| "Can't convert non record avro schema to iceberg schema: {avro_schema}" |
There was a problem hiding this comment.
This is the interpolation from last round — still riding along silently. {avro_schema} printed verbatim before; through the macro's format! arm it now actually interpolates, and it only compiles because AvroSchema: Display happens to hold.
I'm not asking to revert it, it's a strictly better message. Just make it deliberate — a line in the description, or {avro_schema:?} — so it's on the record rather than an accident. wdyt?
There was a problem hiding this comment.
I've update the PR description, hope that works for you!
| macro_rules! invalid_data { | ||
| // Bare literals stay in `format!` so inline captures like `{id}` still interpolate. | ||
| ($fmt: literal $(, $($arg:tt)*)?) => { | ||
| $crate::error::Error::new($crate::error::ErrorKind::DataInvalid, format!($fmt $(, $($arg)*)?)) |
There was a problem hiding this comment.
This is the one new thing I'd want to nail down before merge. The literal arm always routes through format!, so every zero-arg call — invalid_data!("File already closed") and the ~30 others like it — expands to format!("File already closed"), which is exactly what clippy::useless_format fires on. CI runs clippy with -D warnings, so if it surfaces from the expansion the build goes red.
Whether it actually surfaces through a macro_rules! expansion is version-dependent, so the real question is just: is clippy green on this PR? If it is, I'm happy to leave the single-arm design as-is. If not, I'd add a zero-arg arm ahead of the variadic one:
($msg: literal) => {
$crate::error::Error::new($crate::error::ErrorKind::DataInvalid, $msg.to_owned())
};(the trade-off being that inline-capture calls like invalid_data!("text {var}") would then need the explicit invalid_data!("text {}", var) form). wdyt?
| /// | ||
| /// The `use` path below is crate-internal and only resolves inside this crate. | ||
| /// | ||
| /// ```ignore |
There was a problem hiding this comment.
We should not ignore this, we should compile it.
There was a problem hiding this comment.
The macro is pub(crate) use, so a doctest — which compiles as an external crate — can't resolve it. Should we make it pub?
| /// // Attaching a source error | ||
| /// let n: i32 = s.parse().map_err(|e| invalid_data!("not an int: {s}").with_source(e))?; | ||
| /// ``` | ||
| macro_rules! invalid_data { |
There was a problem hiding this comment.
nit, should we have an arm which includes exception so that we don't need to write with_source?
There was a problem hiding this comment.
invalid_data!("msg").with_source(e) → invalid_data!("msg", source = e)
Like this?
| $crate::error::Error::new($crate::error::ErrorKind::DataInvalid, $msg) | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
I see a lot of places has repeated return Err(invalid_data!()), should we have a macro for it as well?
There was a problem hiding this comment.
Could we do this kind of change as a follow up? It would be a reasonable change on top of this PR I think
laskoviymishka
left a comment
There was a problem hiding this comment.
the avro {avro_schema} interpolation being called out as an intentional fix in the description is exactly what I was after — that closes the last thing I had open on it.
with scope complete (delete_vector and the reader pipeline are both in now) and clippy green on the latest run, everything I gated on across the earlier rounds is resolved, and the crate-internal single-arm design is fine as-is.
two small non-blocking things if you feel like it: the FixedSizeBinary path swaps arrow's error text for a static message (the source is preserved, so it's actually a nicer shape — I'd just mention it in the description alongside the avro note), and a tiny test pinning the avro interpolation would keep it from silently regressing. neither holds up the merge.
this is good to land. thanks for sticking with the iteration on this one.
| (PrimitiveType::Fixed(_), PrimitiveLiteral::Binary(value)) => { | ||
| let array = FixedSizeBinaryArray::try_from_iter(std::iter::once(value.as_slice())) | ||
| .map_err(|e| Error::new(ErrorKind::DataInvalid, e.to_string()))?; | ||
| .map_err(|e| invalid_data!("FixedSizeBinary conversion failed").with_source(e))?; |
There was a problem hiding this comment.
this one quietly changes the visible message — it used to surface arrow's own error text as .message(), now it's the static "FixedSizeBinary conversion failed" with the arrow error tucked into .with_source(e).
I actually think the new shape is better since the source chain keeps everything, it's just the one spot in an otherwise mechanical pass that changes what a caller sees. I'd add a line to the PR description calling it out alongside the avro fix so it's not a silent behaviour change. wdyt?
| ErrorKind::DataInvalid, | ||
| "Can't convert non record avro schema to iceberg schema: {avro_schema}", | ||
| Err(invalid_data!( | ||
| "Can't convert non record avro schema to iceberg schema: {avro_schema}" |
There was a problem hiding this comment.
this is the interpolation fix from the description landing — {avro_schema} was riding along as literal text before and now actually renders through the macro's format! arm.
since it's the one real behaviour change in the PR, could we add a small test that runs avro_schema_to_schema on a non-record schema (a bare AvroSchema::Boolean would do) and asserts the message contains the schema's Display rather than the literal {avro_schema}? that locks the fix in so it can't quietly regress. not blocking.
| /// | ||
| /// The `use` path below is crate-internal and only resolves inside this crate. | ||
| /// | ||
| /// ```ignore |
There was a problem hiding this comment.
re: the open thread about compiling this instead of ignore — I think ignore is right here. doctests compile as their own external crate, so they can't resolve a pub(crate) item no matter the toolchain, and making the macro pub just to green the doctest would contradict the "deliberately crate-internal" line right above it. I'd leave it as-is and close that thread. wdyt?
|
I think that's all sorted now, thanks for the reviews! |
|
CI blocked on #3249 |
…/invalid_data_macro
Which issue does this PR close?
Today we construct
DataInvalidlike so: which ends up being very verbose in my opinionWhat changes are included in this PR?
Introduce
invalid_data!macro as a shorthand for this error.The error above becomes:
The migration is behaviour-preserving except at two sites, where the error message changes:
avro_schema_to_schemaincrates/iceberg/src/avro/schema.rs— an intentional fix. Itpreviously passed
"Can't convert non record avro schema to iceberg schema: {avro_schema}"as aplain
&str, so the braces printed verbatim andavro_schemawas never substituted. The macro'sliteral arm routes the message through
format!, so the message now actually interpolates theschema via
Display.get_arrow_datumincrates/iceberg/src/arrow/schema.rs— theFixedSizeBinaryconversion.The failure path was
Error::new(ErrorKind::DataInvalid, e.to_string()), which made arrow's errortext the
message()and left the source unset. It is nowinvalid_data!("FixedSizeBinary conversion failed").with_source(e), so the message is a stablestatic string and arrow's text is preserved as the error's source rather than being flattened into
the message. Nothing is lost, but
message()no longer contains arrow's text — callers matchingon it should read the source instead.
Every
.with_context(...)call on a migrated error is preserved as-is; only the two messages abovediffer.
Are these changes tested?
AI Disclosure
Claude wrote the script to do this migration but I have reviewed manually.