Skip to content

fix(partition): support map keys in put_newer#21

Open
SAY-5 wants to merge 1 commit into
appcues:mainfrom
SAY-5:fix/put-newer-map-key
Open

fix(partition): support map keys in put_newer#21
SAY-5 wants to merge 1 commit into
appcues:mainfrom
SAY-5:fix/put-newer-map-key

Conversation

@SAY-5

@SAY-5 SAY-5 commented Jun 18, 2026

Copy link
Copy Markdown

Closes #18.

put_newer/5 raised ArgumentError ("not a valid match specification") whenever the key contained a map, because ETS rejects a raw map at the key position of a select_replace/2 match head. This binds the key to a match variable and compares it in the guard instead, which works for any key shape. Added a regression test alongside the existing tuple/list-key cases and updated the changelog.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where certain update operations would raise an error when entry keys were maps. Map keys are now correctly validated and handled during entry updates, enabling users to work with complex map-based keys in update operations without encountering validation errors.

Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

replace_match_spec/3 in partition.ex is updated so that the ETS match-spec head no longer embeds the entry key as a raw literal. Instead, the key is captured via a match variable ("$3") and equality is enforced in the guard using ms_literal(key). This avoids the ArgumentError ETS raises when a raw map appears in a match-spec head during :ets.select_replace/2. A regression test covering put_newer/5 with a map key is added to map_test.exs, and the fix is recorded in CHANGELOG.md with a reference to issue #18.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main fix: enabling map key support in the put_newer function.
Linked Issues check ✅ Passed All coding requirements from issue #18 are met: the fix binds the key to a match variable in the guard, addresses the ArgumentError crash with map keys, and includes a regression test.
Out of Scope Changes check ✅ Passed All changes directly address the stated objective of fixing put_newer to handle map-containing keys without workarounds.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/partitioned_buffer/partition.ex`:
- Around line 520-538: The current implementation of replace_match_spec uses a
guard-based key equality check with key: :"$3", which prevents ETS from using
its O(1) keyed lookup optimization on :set tables and forces a full table scan
instead. To fix this, implement a conditional match spec that uses the original
literal-key approach in the match head for non-map keys (which enables the fast
lookup), and only falls back to the guard-based approach with ms_literal(key)
for keys that contain maps (which would otherwise cause ETS to reject the match
spec). This way, the fast path is preserved for the common case while still
handling map-containing keys correctly.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d511c27-ffd9-4e7b-89b2-7cca28c7c652

📥 Commits

Reviewing files that changed from the base of the PR and between fdcd7b0 and f56e892.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • lib/partitioned_buffer/partition.ex
  • test/partitioned_buffer/map_test.exs

Comment on lines 520 to 538
defp replace_match_spec(key, value, version) do
# Performance note: The key in the match head is a literal (bound value),
# not a pattern variable. This allows ETS to use its hash index for O(1)
# lookup rather than scanning the entire table.
# The key is bound to "$3" and compared in the guard rather than embedded
# as a literal in the match head. ETS rejects raw maps in a match head at
# the key position (see :ets.select_replace/2), so keys that contain a map
# would raise "not a valid match specification". Comparing via the guard
# works for any term shape.
#
# In match spec bodies, bare tuples are interpreted as operations/function
# calls, NOT as literal data. We wrap key and value with ms_literal/1 so
# tuples use the {{...}} constructor form and maps use {:const, map} that
# ETS understands. This handles tuples, maps, and lists (including nested
# combinations).
# In match spec bodies and guards, bare tuples are interpreted as
# operations/function calls, NOT as literal data. We wrap key and value
# with ms_literal/1 so tuples use the {{...}} constructor form and maps use
# {:const, map} that ETS understands. This handles tuples, maps, and lists
# (including nested combinations).
[
{
# Match: {entry, key, value, existing_version, updates} where key is literal
entry(key: key, value: :_, version: :"$1", updates: :"$2"),
# Guard (update only if): new_version > existing_version
[{:>, version, :"$1"}],
# Match: {entry, key, value, existing_version, updates}
entry(key: :"$3", value: :_, version: :"$1", updates: :"$2"),
# Guard (update only if): key matches and new_version > existing_version
[{:>, version, :"$1"}, {:"=:=", :"$3", ms_literal(key)}],
# Result: the new entry with incremented updates counter

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Erlang/OTP docs for :ets.select_replace/2, does using a key match variable plus guard equality disable key-based lookup optimization versus a literal key in the match head for :set tables?

💡 Result:

Yes, using a key match variable in the match head instead of a literal key does disable key-based lookup optimization for:set tables [1][2][3]. In Erlang/OTP, ETS match specifications (used by functions such as:ets.select_replace/2) rely on the match head to optimize search operations [4][2]. The documentation explicitly states that a match pattern with a fully bound key—meaning a key specified as a literal without any match variables—is optimized to a single key lookup, bypassing the need for a full table scan [1][2]. If the key position in the match head contains a match variable (e.g., '$1') or an underscore ('_'), the system cannot perform a direct key-based lookup [2][3]. Moving an equality check (e.g., K =:= 123) into the match specification's guard does not restore this optimization because the guard is only evaluated after an object has already been matched and extracted from the table, often necessitating a scan of the entire table [4][5]. Consequently, for large:set tables, using a variable in the match head will result in a significantly less efficient full table scan compared to using the exact key [1][3].

Citations:


Guard-only key equality disables ETS keyed lookup optimization.

The change to use key: :"$3" with guard equality removes the literal-key match head that enables O(1) lookups on :set tables. Even though the guard binds the key, ETS still evaluates the guard after extracting candidates, forcing a full scan instead of a direct key lookup. For put_newer/2 on existing keys, this degrades performance significantly. Consider a conditional match spec: use the literal-key head for non-map keys (where the original code worked) and the guard-based path only for map-containing keys that triggered the crash.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/partitioned_buffer/partition.ex` around lines 520 - 538, The current
implementation of replace_match_spec uses a guard-based key equality check with
key: :"$3", which prevents ETS from using its O(1) keyed lookup optimization on
:set tables and forces a full table scan instead. To fix this, implement a
conditional match spec that uses the original literal-key approach in the match
head for non-map keys (which enables the fast lookup), and only falls back to
the guard-based approach with ms_literal(key) for keys that contain maps (which
would otherwise cause ETS to reject the match spec). This way, the fast path is
preserved for the common case while still handling map-containing keys
correctly.

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.

put_newer crashes with "not a valid match specification" when the key contains a map

1 participant