-
Notifications
You must be signed in to change notification settings - Fork 1k
fix: Incorrect inlined string view comparison after Add prefix compar… #7875
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
cc @alamb @Dandandan |
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.
Thank you @zhuqi-lucas -- I verified this fixes the issue I was seeing in DataFusion 🙏
Given the subtlety here, I think a few more tests are warranted -- I left some more suggestions. Let's try and make sure we are 100% sure about this one
// This pair verifies that we didn’t accidentally reverse the inline bytes: | ||
// without our fix, “backend one” would compare as if it were | ||
// “eno dnekcab”, so “one” might end up sorting _after_ “two”. | ||
b"backend one", // special case: tests byte-order reversal bug |
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 wonder why this wasn't this caught withthe xyy
and xyz
test 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.
Good question, because :
The bug caused full byte reversal of the inline string bytes, meaning the entire 12-byte segment was reversed before comparison.
For strings like "xyy" and "xyz", which differ only in their last byte, reversing the bytes moves this difference to the first byte of the reversed string.
Since comparisons are done on the reversed bytes for both strings, the order is consistently flipped but preserved between them.
Thus, even though the byte order is wrong globally (the entire string is reversed), "xyy" still compares correctly as less than "xyz" in the reversed space, so the test passes.
In other words, differences at the end of short strings don’t expose the reversal bug, because reversing the entire string simply moves the difference to the front, preserving the relative order.
The bug only becomes apparent in strings with differences in the middle or earlier bytes, like "backend one" vs "backend two", where reversing the entire inline data inverts the lexicographical order unexpectedly.
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 because"xyy" < "xyz"
and "yxx" < "zyx"
?
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 because
"xyy" < "xyz"
and"yxx" < "zyx"
?
Right!
// without our fix, “backend one” would compare as if it were | ||
// “eno dnekcab”, so “one” might end up sorting _after_ “two”. | ||
b"backend one", // special case: tests byte-order reversal bug | ||
b"backend two", |
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.
Can we also please include strings that have the same exact inline prefix the length differ as well as content ?
Something like
LongerThan12Bytes
LongerThan12Bytez
LongerThan12Bytes\0
LongerThan12Byt
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.
Also, given the endian swapping going on, can we please also include a few strings that are more than 256 bytes long (so the length requires 2 bytes to store)?
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.
Thank you @alamb for good point:
b"than12Byt",
b"than12Bytes",
b"than12Bytes\0",
b"than12Bytez",
I will add above cases, because inline_key_fast function only used for
<= 12 and <= 12 bytes to 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.
Added in latest PR for above testing 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.
Thank you @alamb I also found a way to do it, i adjust the fuzz testing to reproduce the cases, and this PR will make the fuzz testing works well.
/// ⇒ key("bar") < key("bar\0") | ||
/// ``` | ||
/// | ||
/// ### Why the old code failed |
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 think it would be more helpful to future readers to explain here to explain how the calculation works, rather than explaining why a previous attempt didnt' work 🤔
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 @alamb , thank you!
/// | ||
/// ```text | ||
/// key("bar") = 0x0000000000000000000062617200000003 | ||
/// key("bar\0") = 0x0000000000000000000062617200000004 |
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 think this diagram needs be updated as well
Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>
Thank you @alamb for review, added rich tests and comments in latest change! |
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.
Thank you @zhuqi-lucas -- I also pushed a few more test cases to ease my mind
// | ||
// [0x77, 0x56, 0x34, 0x22] // little-endian layout | ||
// ^ ^ ^ ^ | ||
// LSB ↑↑↑ MSB |
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 a very nice comment
Thank you @alamb , i will also port to datafusion fix for CursorValue compare after this PR merged. |
🤖 |
🤖: Benchmark completed Details
|
Thank you @alamb for benchmark, i think it's ok for the benchmark here, only less than 10% regression for some cases: sort string_view[0-400] nulls to indices 2^12 1.04 89.2±0.16µs ? ?/sec 1.00 85.4±0.13µs ? ?/sec
sort string_view[0-400] to indices 2^12 1.10 133.6±0.21µs ? ?/sec 1.00 121.6±0.22µs ? ?/sec
sort string_view[10] nulls to indices 2^12 1.05 73.7±0.38µs ? ?/sec 1.00 70.5±0.55µs ? ?/sec
sort string_view[10] to indices 2^12 1.00 104.2±0.46µs ? ?/sec 1.00 104.4±0.25µs ? ?/sec
sort string_view_inlined[0-12] nulls to indices 2^12 1.06 70.7±0.28µs ? ?/sec 1.00 66.6±0.26µs ? ?/sec
sort string_view_inlined[0-12] to indices 2^12 1.00 94.5±0.25µs ? ?/sec 1.01 95.0±1.10µs ? ?/sec Because we originally has improved the sort with main for total 2x ~5x faster for the following two PRs: |
I agree -- thanks again @zhuqi-lucas |
Thank you @alamb , submitted the port to datafusion: |
…e for inlined
Which issue does this PR close?
Rationale for this change
Change Summary
Rework
inline_key_fast
to avoid reversing the inline data bytes by removing the global.to_be()
on the entire 128‑bit word and instead manually constructing the big‑endian key in two parts: the 96‑bit data portion and the 32‑bit length tiebreaker.Problem
In the original implementation:
.to_be()
on the full 16‑byte value flips all bytes, including the 12 bytes of inline data."backend one"
would sort as if it were"eno dnekcab"
— so lexicographical ordering is completely inverted.“backend one” vs. “backend two”: suffixes “one”/“two” compare incorrectly once reversed.
Solution
Testing
All existing tests — including the “backend one” vs. “backend two” and
"bar"
vs."bar\0"
cases — now pass, confirming both lexicographical correctness and proper length‑based tiebreaking.What changes are included in this PR?
Are these changes tested?
Yes
Are there any user-facing changes?
No