Skip to content

fix: reorder type checks after stale validation and fix zset store destination handling - #261

Open
guozhihao-224 wants to merge 6 commits into
mainfrom
fix/type_check
Open

fix: reorder type checks after stale validation and fix zset store destination handling#261
guozhihao-224 wants to merge 6 commits into
mainfrom
fix/type_check

Conversation

@guozhihao-224

@guozhihao-224 guozhihao-224 commented Apr 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Reorder type checks to occur after stale validation across set, string, and sorted-set operations, ensuring correct WRONGTYPE responses for live non-matching keys while treating stale keys as non-existent.

Key changes

  • Reorder stale/type check flow (set, zset): Replace is_valid() with centralized self.is_stale() gating, and move check_type() to after staleness validation. This ensures stale keys are treated as absent before any type check runs.
  • Fix zadd meta parsing order: Move ParsedZSetsMetaValue::new() after is_stale()/check_type() to prevent wrong error for non-ZSet keys.
  • Fix ZINTERSTORE/ZUNIONSTORE destination cleanup: Replace early return Ok(0) with a flag + break so the destination lock and cleanup logic always executes, preventing stale destination data from surviving.
  • Fix ZINTERSTORE/ZUNIONSTORE destination type check: Remove check_type on destination key — per Redis semantics, store commands overwrite the destination regardless of its current type.
  • Fix mget for expired entries: Short-circuit on stale values, returning None without attempting UTF-8 decode.

Tests added

  • scard returns WRONGTYPE for a live string key
  • zcard returns WRONGTYPE for a live string key
  • mget returns None for wrong-type and missing keys

Summary by CodeRabbit

  • Bug Fixes

    • Consistently treat expired/stale keys as absent across set and sorted-set operations, yielding empty results or not-found errors and avoiding unnecessary parsing.
    • Fix move/merge behaviors to correctly handle stale destinations and preserve member encoding.
    • Short-circuit string multi-get to skip decoding for expired keys.
  • Tests

    • Added integration tests covering type-mismatch, move semantics, expired-key handling, and multi-key retrieval behaviors.

@coderabbitai

coderabbitai Bot commented Apr 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Gate Redis Set/ZSet/String operations on metadata staleness (self.is_stale), move type checks after stale validation, update smove member-key encoding, and add tests for wrong-type and stale behaviors.

Changes

Staleness gating and set/zset fixes

Layer / File(s) Summary
Set ops — staleness-first gating and smove fix
src/storage/src/redis_sets.rs
Reordered set paths to check self.is_stale(&val)? before check_type/ParsedSetsMetaValue::new; stale metas short-circuit (empty/false/KeyNotFound); smove source/dest now use staleness gates and use MemberDataKey::new(...).encode() for member keys.
ZSet ops — staleness-first gating and zset_store changes
src/storage/src/redis_zsets.rs
Replaced is_valid() guards with self.is_stale(&base_meta_val)? gating across zset reads/writes; stale metas are treated as absent (e.g., zincrbyzadd); zset_store_operation adds result_empty handling and refines destination-deletion predicates.
String ops — mget short-circuit
src/storage/src/redis_strings.rs
mget now continues early for stale string entries and only decodes UTF‑8 for non-stale values.
Tests — Sets
src/storage/tests/redis_set_test.rs
Adds tests: scard wrong-type error, smove basic move behavior, and smove wrong-type destination error.
Tests — Strings
src/storage/tests/redis_string_test.rs
Adds test_redis_mget_wrong_type_returns_nil_entry to verify mget returns None for wrong-type/missing keys.
Tests — ZSets
src/storage/tests/redis_zset_test.rs
Adds zset wrong-type and stale destination overwrite tests for zcard, zadd, zinterstore, and zunionstore.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • arana-db/kiwi#190: Related is_stale(...) expiry gating relied upon here.
  • arana-db/kiwi#262: MemberDataKey encoding changes used by updated set member-key construction.
  • arana-db/kiwi#126: Earlier set-collection/key-version refactor that touches sdiff/sinter logic modified here.

Suggested labels

☢️ Bug

Suggested reviewers

  • AlexStocks
  • marsevilspirit

Poem

🐰 I hopped through meta, sniffed the stale scent,
Skipped expired crumbs and only parsed what's meant.
I moved members tidy, encoded keys just right,
Tests sing "WRONGTYPE" when types give a fright.
A little rabbit hop—now the gates close tight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: reordering type checks after stale validation and fixing zset store destination handling, which align with the primary objectives of the PR.
Description check ✅ Passed The PR description includes a summary, key changes explaining the technical approach, and tests added. However, the template's Type of Change and Checklist sections are not completed or filled out.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/type_check

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/storage/src/redis_zsets.rs (1)

1143-1160: ⚠️ Potential issue | 🟠 Major

Don’t return before clearing the destination for empty intersections.

For ZINTERSTORE, a missing or stale source means the result is empty, but the destination still needs to be overwritten. These early return Ok(0) paths skip the destination cleanup/write block entirely, so an existing destination can survive even though the command reports 0.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/storage/src/redis_zsets.rs` around lines 1143 - 1160, The early returns
in the ZINTERSTORE handling (the branches checking base_meta_val.is_empty() and
self.is_stale(&base_meta_val) that currently do `return Ok(0)` when is_inter is
true) skip the destination cleanup/write and leave an old destination intact;
change these returns to instead set a flag (e.g., `empty_result = true`) or
update the loop state and break out so processing continues to the common
destination-write/clear logic, and then ensure the destination is
overwritten/cleared and returns 0 at the end; update the branches in the
function handling ZINTERSTORE (use the `is_inter` check, the `base_meta_val`
empty/stale branches, and the existing destination write/cleanup code) to follow
this flow so destination is always cleared when intersection yields empty.
🧹 Nitpick comments (1)
src/storage/src/redis_sets.rs (1)

802-806: Consider simplifying the is_valid() check.

After the stale check at line 732, if dest_exists is true and the destination was stale, initial_meta_value() was called which resets the metadata (including count to 0 and clearing expiration). The is_valid() check here may be redundant since:

  • If dest was stale → initial_meta_value() resets etime → is_valid() returns true, count is 0 → 0 + 1 = 1
  • If dest was not stale → is_valid() returns true, count is existing value → count + 1

The current code works correctly, but the condition could potentially be simplified to track whether the destination was stale explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/storage/src/redis_sets.rs` around lines 802 - 806, The is_valid() check
after handling a stale destination is redundant; instead, track whether you
reset the destination metadata (e.g., via a boolean like dest_was_stale) when
calling initial_meta_value() and then unconditionally set the new count: if
dest_exists { if dest_was_stale { dest_meta.set_count(1) } else {
dest_meta.set_count(dest_meta.count() + 1) } } else { dest_meta.set_count(1) } —
update the logic around dest_meta, dest_exists, initial_meta_value(),
is_valid(), set_count(), and count() so the code no longer relies on is_valid()
to distinguish stale vs. fresh state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/storage/src/redis_zsets.rs`:
- Around line 82-93: Parsed zset-meta is being constructed before we verify the
live key type, which causes non-zset live keys to error during meta parsing;
move the call to ParsedZSetsMetaValue::new(...) so it occurs after
check_type(&base_meta_val, DataType::ZSet) and the is_stale check, i.e., first
call self.is_stale(&base_meta_val) and self.check_type(...) to ensure WRONGTYPE
is returned for live non-zset keys, then parse with
ParsedZSetsMetaValue::new(...); apply the same reorder to the zadd code path so
zadd also performs type and staleness checks before invoking
ParsedZSetsMetaValue::new.

In `@src/storage/tests/redis_set_test.rs`:
- Line 170: Formatting mismatch on the call to redis.set(key, b"x").expect("set
failed"); — run rustfmt to fix the whitespace/formatting by running `cargo fmt
--package storage` (or apply rustfmt to the test file) so the line conforms to
project formatting; target the test containing the redis.set call to reformat
and then re-run tests.

In `@src/storage/tests/redis_string_test.rs`:
- Line 350: Formatting mismatch on the `.hset(hash_key, b"field", b"value")`
call in the Redis string tests: run rustfmt to fix it (e.g., run `cargo fmt
--package storage`) or apply rustfmt formatting to the test containing the
`.hset` invocation so the line matches project formatting; ensure the `hset`
call and surrounding test are formatted by rustfmt and commit the updated file.

---

Outside diff comments:
In `@src/storage/src/redis_zsets.rs`:
- Around line 1143-1160: The early returns in the ZINTERSTORE handling (the
branches checking base_meta_val.is_empty() and self.is_stale(&base_meta_val)
that currently do `return Ok(0)` when is_inter is true) skip the destination
cleanup/write and leave an old destination intact; change these returns to
instead set a flag (e.g., `empty_result = true`) or update the loop state and
break out so processing continues to the common destination-write/clear logic,
and then ensure the destination is overwritten/cleared and returns 0 at the end;
update the branches in the function handling ZINTERSTORE (use the `is_inter`
check, the `base_meta_val` empty/stale branches, and the existing destination
write/cleanup code) to follow this flow so destination is always cleared when
intersection yields empty.

---

Nitpick comments:
In `@src/storage/src/redis_sets.rs`:
- Around line 802-806: The is_valid() check after handling a stale destination
is redundant; instead, track whether you reset the destination metadata (e.g.,
via a boolean like dest_was_stale) when calling initial_meta_value() and then
unconditionally set the new count: if dest_exists { if dest_was_stale {
dest_meta.set_count(1) } else { dest_meta.set_count(dest_meta.count() + 1) } }
else { dest_meta.set_count(1) } — update the logic around dest_meta,
dest_exists, initial_meta_value(), is_valid(), set_count(), and count() so the
code no longer relies on is_valid() to distinguish stale vs. fresh state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a00e8307-6d0e-4a7b-88c0-5c1aeda43102

📥 Commits

Reviewing files that changed from the base of the PR and between 60cd512 and 73b4f0f.

📒 Files selected for processing (6)
  • src/storage/src/redis_sets.rs
  • src/storage/src/redis_strings.rs
  • src/storage/src/redis_zsets.rs
  • src/storage/tests/redis_set_test.rs
  • src/storage/tests/redis_string_test.rs
  • src/storage/tests/redis_zset_test.rs

Comment thread src/storage/src/redis_zsets.rs Outdated
Comment thread src/storage/tests/redis_set_test.rs
Comment thread src/storage/tests/redis_string_test.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/storage/src/redis_zsets.rs (1)

82-93: ⚠️ Potential issue | 🟠 Major

zadd still parses ZSET metadata before stale/type gating.

Line 82 constructs ParsedZSetsMetaValue before the Line 87 stale check and the Line 91 type check, so a non-ZSET key can still fail meta parsing before this path returns WRONGTYPE or treats an expired key as absent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/storage/src/redis_zsets.rs` around lines 82 - 93, ParsedZSetsMetaValue is
being constructed before gating by is_stale and check_type in the zadd path,
which can cause meta parsing to error for non-ZSET or expired keys; change the
order in the zadd implementation so you first call
self.is_stale(&base_meta_val)? and if not stale call
self.check_type(&base_meta_val, DataType::ZSet)? before constructing
ParsedZSetsMetaValue, and for the stale branch use ParsedZSetsMetaValue::new
only if you need parsed fields (or use parsed_zset_meta.initial_meta_value()
only after safe parsing), ensuring any use of parsed_zset_meta happens after the
type/stale checks to avoid premature parsing errors (look for
ParsedZSetsMetaValue, is_stale, check_type, and the zadd function to update).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/storage/src/redis_zsets.rs`:
- Around line 1154-1159: In the zinterstore/zunionstore scan logic, remove the
early return Ok(0) when encountering a stale or missing source (the branch that
currently returns for is_inter) and instead set a local flag (e.g., result_empty
= true) and fall through to the existing destination-delete/write path so the
destination lock acquisition and cleanup code (the destination lock and final
cleanup block) still run; apply the same change to the "missing-key" branch so
both cases mark the result as empty and continue rather than returning
prematurely, then have the final write/delete logic check result_empty and
perform the delete/empty-write behavior before returning 0.
- Around line 1225-1227: The code currently calls
self.check_type(&dest_meta_val, DataType::ZSet) and rejects non-ZSET destination
keys, but Redis semantics overwrite any existing type; remove the unconditional
check_type call and instead only perform ZSET-specific physical cleanup when the
existing destination is actually a ZSET: check if dest_meta_val is non-empty and
not stale (using self.is_stale), then attempt to parse
ParsedZSetsMetaValue::new(&dest_meta_val[..]) and if parsing succeeds treat it
as an existing ZSET and run the cleanup logic, otherwise skip cleanup and
proceed with the overwrite unconditionally (do not return WRONGTYPE).

---

Duplicate comments:
In `@src/storage/src/redis_zsets.rs`:
- Around line 82-93: ParsedZSetsMetaValue is being constructed before gating by
is_stale and check_type in the zadd path, which can cause meta parsing to error
for non-ZSET or expired keys; change the order in the zadd implementation so you
first call self.is_stale(&base_meta_val)? and if not stale call
self.check_type(&base_meta_val, DataType::ZSet)? before constructing
ParsedZSetsMetaValue, and for the stale branch use ParsedZSetsMetaValue::new
only if you need parsed fields (or use parsed_zset_meta.initial_meta_value()
only after safe parsing), ensuring any use of parsed_zset_meta happens after the
type/stale checks to avoid premature parsing errors (look for
ParsedZSetsMetaValue, is_stale, check_type, and the zadd function to update).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e3a6d039-60e7-49d2-9c95-3e2b7343a685

📥 Commits

Reviewing files that changed from the base of the PR and between 73b4f0f and a55f171.

📒 Files selected for processing (4)
  • src/storage/src/redis_zsets.rs
  • src/storage/tests/redis_set_test.rs
  • src/storage/tests/redis_string_test.rs
  • src/storage/tests/redis_zset_test.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/storage/tests/redis_zset_test.rs
  • src/storage/tests/redis_string_test.rs

Comment thread src/storage/src/redis_zsets.rs
Comment thread src/storage/src/redis_zsets.rs Outdated
@guozhihao-224 guozhihao-224 changed the title refactor: Correct the order of type_check fix: reorder type checks after stale validation and fix zset store destination handling Apr 18, 2026
@ruojieranyishen

Copy link
Copy Markdown
Contributor

@guozhihao-224 辛苦处理CI问题

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adjusts Redis-compatible stale-key handling so expired keys are treated as absent before type validation, while live keys of the wrong type still return the expected behavior.

Changes:

  • Reordered stale/type checks across set and sorted-set operations.
  • Updated MGET handling for wrong-type/missing entries.
  • Added regression tests for set, sorted-set, and string command behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/storage/src/redis_zsets.rs Reorders stale/type validation and updates zset store destination handling.
src/storage/src/redis_sets.rs Reorders stale/type validation for set operations.
src/storage/src/redis_strings.rs Adjusts MGET nil handling flow.
src/storage/tests/redis_zset_test.rs Adds ZCARD wrong-type regression coverage.
src/storage/tests/redis_set_test.rs Adds SCARD wrong-type regression coverage.
src/storage/tests/redis_string_test.rs Adds MGET wrong-type and missing-key coverage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/storage/src/redis_zsets.rs Outdated
Comment thread src/storage/src/redis_sets.rs Outdated
Comment thread src/storage/src/redis_zsets.rs
Comment thread src/storage/tests/redis_zset_test.rs

@AlexStocks AlexStocks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审计结论:发现 2 个需要处理的 [P1] 项,均已落到 diff 行评论。

补充说明:

  • cargo clippy --all-features --workspace -- -D warnings -D clippy::unwrap_used 未能产出代码级结果,当前环境缺少 dlltool.exe,构建在 getrandom 依赖处中断。
  • 本次 review 保持只读,未修改源码。

guozhihao-224 and others added 5 commits June 6, 2026 20:15
- Move ParsedZSetsMetaValue::new() after is_stale/check_type in zadd
  to prevent wrong error for non-ZSet keys
- Replace early return Ok(0) in zinterstore/zunionstore with break +
  flag so destination cleanup always runs
- Remove check_type on destination key in zinterstore/zunionstore
  since Redis overwrites destination regardless of its current type

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- redis_zsets.rs (zadd): when the existing meta is stale, build a fresh
  ZSetsMetaValue instead of reparsing the old buffer. The previous code
  preserved the original data_type byte (Hash/Set/etc.), so persisting it
  back broke subsequent ZSet ops with WRONGTYPE.
- redis_sets.rs (smove): gate the destination meta on is_stale and
  check_type before parsing, so a non-Set value returns WRONGTYPE rather
  than InvalidFormat. Stale destinations are rewritten as fresh Set meta.
- redis_sets.rs (smove): switch source/destination member-key encoding
  from a hand-rolled order (version before key) to MemberDataKey, which
  matches what sadd writes; smove could not previously locate any member
  written by sadd.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ssion tests

- Fix import order in redis_zset_test.rs: std first, then external crates
  (bytes, kstd), then project-internal storage:: imports
- Add test_zinterstore_dest_live_wrong_type_is_overwritten: verifies that
  ZINTERSTORE silently overwrites a live wrong-type destination key
- Add test_zunionstore_dest_stale_wrong_type_is_overwritten: verifies that
  ZUNIONSTORE treats a stale wrong-type destination as absent and succeeds

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
base_meta_value_format → format_base_meta_value
base_value_format      → format_base_value

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants