Skip to content

Fix duplicate row ID on concurrent row insertion#127

Merged
hackerwins merged 1 commit into
mainfrom
fix/random-axis-id
Apr 13, 2026
Merged

Fix duplicate row ID on concurrent row insertion#127
hackerwins merged 1 commit into
mainfrom
fix/random-axis-id

Conversation

@hackerwins

@hackerwins hackerwins commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Root cause: createWorksheetAxisId() used a shared nextRowId counter. When two clients inserted rows simultaneously, both read the same counter value and generated identical IDs (e.g., both create r93), leaving a duplicate in rowOrder.
  • Observed symptom: In production document sheet-a39621d7, r93 appeared at both visual row 15 and row 30. Editing row 15 caused changes to appear at row 30.
  • Fix: Replace sequential counter with 4-char base36 random IDs via crypto.getRandomValues (~1.6M combinations), making collisions structurally impossible regardless of concurrency.

Changed files

File Change
packages/sheets/src/model/workbook/worksheet-record.ts createWorksheetAxisId() → random ID generation
packages/sheets/src/model/workbook/worksheet-grid.ts Remove counter dependency from ensureAxisLength()
packages/frontend/src/app/spreadsheet/yorkie-worksheet-axis.ts Remove counter dependency from ensureAxisLength() + insertYorkieWorksheetAxis()
packages/sheets/test/model/worksheet-record.test.ts New: ID format + uniqueness tests

Test plan

  • pnpm verify:fast passes (1115 sheets tests + all other packages)
  • Existing concurrency-matrix tests pass
  • New worksheet-record.test.ts validates ID format (/^[rc][a-z0-9]{4}$/) and uniqueness across 1000 calls
  • Manual: verify concurrent row insertion in shared document no longer produces duplicate IDs

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Resolved critical production issue where concurrent row insertions could generate duplicate worksheet axis identifiers. ID generation now ensures uniqueness during simultaneous operations.
  • Tests

    • Added tests validating unique ID generation and collision detection across concurrent insertion scenarios.
  • Documentation

    • Added task documentation outlining the production issue and implementation approach.

@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@hackerwins has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 31 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 12 minutes and 31 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fa34ea4a-ea91-4be4-b1c9-6a0d2145166e

📥 Commits

Reviewing files that changed from the base of the PR and between 78adea1 and cd18ed9.

📒 Files selected for processing (5)
  • docs/tasks/active/20260413-random-axis-id-todo.md
  • packages/frontend/src/app/spreadsheet/yorkie-worksheet-axis.ts
  • packages/sheets/src/model/workbook/worksheet-grid.ts
  • packages/sheets/src/model/workbook/worksheet-record.ts
  • packages/sheets/test/model/worksheet-record.test.ts
📝 Walkthrough

Walkthrough

The changes address a production issue where concurrent worksheet axis insertions could generate duplicate row IDs by replacing sequential ID generation with random 4-character base36 identifiers. The function signature and all call sites are updated to remove counter dependencies.

Changes

Cohort / File(s) Summary
Random Axis ID Implementation
packages/sheets/src/model/workbook/worksheet-record.ts
Modified createWorksheetAxisId() to generate random 4-character base36 suffixes instead of sequential numeric suffixes; removed index parameter from function signature.
Worksheet Grid & Axis Integration
packages/sheets/src/model/workbook/worksheet-grid.ts, packages/frontend/src/app/spreadsheet/yorkie-worksheet-axis.ts
Removed per-axis counter state updates and reliance on incrementing numeric values; updated ensureAxisLength() and insertYorkieWorksheetAxis() to call createWorksheetAxisId() without counter parameters.
Tests & Documentation
packages/sheets/test/model/worksheet-record.test.ts, docs/tasks/active/20260413-random-axis-id-todo.md
Added test suite validating ID format and collision uniqueness across 1000 iterations; added task documentation describing the production issue and follow-up work items.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Random whispers in the spreadsheet night,
No more counters causing duplicate fright,
Base36 chaos, four characters wild,
Concurrent inserts smile, reconciled!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing duplicate row IDs caused by concurrent row insertion by switching from a sequential counter to random IDs.

✏️ 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/random-axis-id

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.

@github-actions

github-actions Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 119.6s

Lane Status Duration
sheets:build ✅ pass 13.2s
docs:build ✅ pass 8.0s
verify:fast ✅ pass 60.7s
frontend:build ✅ pass 15.9s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 4.9s
cli:build ✅ pass 1.9s
verify:entropy ✅ pass 14.7s

Verification: verify:integration

Result: ✅ PASS

@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

Caution

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

⚠️ Outside diff range comments (1)
packages/sheets/src/model/workbook/worksheet-grid.ts (1)

32-33: 🛠️ Refactor suggestion | 🟠 Major

nextRowId/nextColId are unused—remove them along with migration code that sets them.

These fields are initialized here but never read or used for ID generation; createWorksheetAxisId generates IDs randomly using crypto instead. They're also never updated when ensureAxisLength grows the axes, confirming they serve no functional purpose.

Removing them requires:

  1. Deleting initialization from ensureWorksheetGrid (worksheet-grid.ts:32-33)
  2. Removing from the WorksheetGridShape interface (worksheet-record.ts:7-8)
  3. Removing from WorksheetDocument interface (worksheet-document.ts:49-50)
  4. Updating migration code that sets these fields (worksheet-shape-migration.ts:435-436)
  5. Adjusting migration validation logic (worksheet-shape-migration.ts:316-317)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheets/src/model/workbook/worksheet-grid.ts` around lines 32 - 33,
Remove the unused nextRowId and nextColId fields and related migration handling:
delete the initialization lines in ensureWorksheetGrid that set ws.nextRowId and
ws.nextColId, remove those properties from the WorksheetGridShape and
WorksheetDocument interfaces, and update the migration code and validation in
the worksheet-shape-migration logic to stop reading/writing these fields (they
are unused because createWorksheetAxisId generates IDs and ensureAxisLength
updates axes). Ensure no remaining references to nextRowId/nextColId exist after
the changes.
🧹 Nitpick comments (3)
docs/tasks/active/20260413-random-axis-id-todo.md (2)

10-14: Update task checkboxes to reflect completed work.

Based on the code changes in this PR:

  • Tasks 1-3 appear to be implemented
  • Task 4 (concurrency test) is partially addressed—there's a uniqueness test but not a true concurrent insertion test
  • Task 5 status is mentioned as passing in the PR description

Consider marking completed items or removing this file if merging.

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

In `@docs/tasks/active/20260413-random-axis-id-todo.md` around lines 10 - 14,
Update the checklist in this task file to reflect implemented changes: mark
items 1, 2, and 3 as completed (since createWorksheetAxisId, ensureAxisLength,
and insertYorkieWorksheetAxis were changed), adjust item 4 to note that the
uniqueness test exists but a true concurrency test for simultaneous inserts is
still needed (or remove/replace it if you add the concurrent test separately),
and mark item 5 as passed if pnpm verify:fast succeeded; alternatively remove
the file if it's no longer needed.

1-14: Consider adding a paired lessons file.

As per coding guidelines, non-trivial tasks in docs/tasks/active/ should use paired files: YYYYMMDD-<slug>-todo.md and YYYYMMDD-<slug>-lessons.md. A lessons file documenting root cause analysis and the reasoning behind choosing random IDs over other solutions would be valuable for future reference.

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

In `@docs/tasks/active/20260413-random-axis-id-todo.md` around lines 1 - 14, The
review requests adding a paired lessons file alongside the todo: create a new
file named with the same date/slug but ending in -lessons.md that documents the
root cause (shared nextRowId counter causing concurrent duplicate axis IDs), why
we chose 4-char base36 random IDs in createWorksheetAxisId(prefix, number)
instead of other approaches, how ensureAxisLength() and
insertYorkieWorksheetAxis() were updated to remove counter dependency, and
include notes on the concurrency test added and verification steps (pnpm
verify:fast) so future devs can understand the reasoning and reproduce the fix;
reference the functions createWorksheetAxisId, ensureAxisLength, and
insertYorkieWorksheetAxis in the lessons file.
packages/sheets/test/model/worksheet-record.test.ts (1)

13-20: Probabilistic test may flake over time.

With 36^4 ≈ 1.68M possibilities, generating 1000 IDs has a non-trivial collision probability (~0.03% per run via birthday paradox). While unlikely in a single run, this could cause occasional CI failures across many runs.

Consider either:

  1. Lowering the count to reduce collision probability further (e.g., 500 IDs)
  2. Accepting that the test verifies "high uniqueness" rather than absolute guarantees

Also, task 4 in the todo file mentions adding a concurrency test for simultaneous row inserts—this test only validates single-threaded uniqueness.

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

In `@packages/sheets/test/model/worksheet-record.test.ts` around lines 13 - 20,
The probabilistic uniqueness test "should generate unique IDs across many calls"
using createWorksheetAxisId('r') can flake; reduce collision risk by lowering
iterations (e.g., from 1000 to 500) or change the assertion to verify "high
uniqueness" (e.g., expect(ids.size).toBeGreaterThanOrEqual(490)) instead of
exact equality; update the test body that constructs the Set and the final
expect to use the new count or threshold and leave a brief comment referencing
createWorksheetAxisId and the reason for the threshold.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/sheets/src/model/workbook/worksheet-record.ts`:
- Around line 80-81: Add an explicit engines constraint to the package.json for
the packages/sheets package so consumers and CI know the minimum Node version;
update the packages/sheets package.json to include an "engines" field (e.g.,
"node": ">=18" to satisfy Vitest and modern crypto.getRandomValues availability)
and commit the change so tests and downstream consumers are aware of the runtime
requirement.

---

Outside diff comments:
In `@packages/sheets/src/model/workbook/worksheet-grid.ts`:
- Around line 32-33: Remove the unused nextRowId and nextColId fields and
related migration handling: delete the initialization lines in
ensureWorksheetGrid that set ws.nextRowId and ws.nextColId, remove those
properties from the WorksheetGridShape and WorksheetDocument interfaces, and
update the migration code and validation in the worksheet-shape-migration logic
to stop reading/writing these fields (they are unused because
createWorksheetAxisId generates IDs and ensureAxisLength updates axes). Ensure
no remaining references to nextRowId/nextColId exist after the changes.

---

Nitpick comments:
In `@docs/tasks/active/20260413-random-axis-id-todo.md`:
- Around line 10-14: Update the checklist in this task file to reflect
implemented changes: mark items 1, 2, and 3 as completed (since
createWorksheetAxisId, ensureAxisLength, and insertYorkieWorksheetAxis were
changed), adjust item 4 to note that the uniqueness test exists but a true
concurrency test for simultaneous inserts is still needed (or remove/replace it
if you add the concurrent test separately), and mark item 5 as passed if pnpm
verify:fast succeeded; alternatively remove the file if it's no longer needed.
- Around line 1-14: The review requests adding a paired lessons file alongside
the todo: create a new file named with the same date/slug but ending in
-lessons.md that documents the root cause (shared nextRowId counter causing
concurrent duplicate axis IDs), why we chose 4-char base36 random IDs in
createWorksheetAxisId(prefix, number) instead of other approaches, how
ensureAxisLength() and insertYorkieWorksheetAxis() were updated to remove
counter dependency, and include notes on the concurrency test added and
verification steps (pnpm verify:fast) so future devs can understand the
reasoning and reproduce the fix; reference the functions createWorksheetAxisId,
ensureAxisLength, and insertYorkieWorksheetAxis in the lessons file.

In `@packages/sheets/test/model/worksheet-record.test.ts`:
- Around line 13-20: The probabilistic uniqueness test "should generate unique
IDs across many calls" using createWorksheetAxisId('r') can flake; reduce
collision risk by lowering iterations (e.g., from 1000 to 500) or change the
assertion to verify "high uniqueness" (e.g.,
expect(ids.size).toBeGreaterThanOrEqual(490)) instead of exact equality; update
the test body that constructs the Set and the final expect to use the new count
or threshold and leave a brief comment referencing createWorksheetAxisId and the
reason for the threshold.
🪄 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: ee1c6f30-33a0-4c3c-bbc4-21dfecb86581

📥 Commits

Reviewing files that changed from the base of the PR and between 5779065 and 78adea1.

📒 Files selected for processing (5)
  • docs/tasks/active/20260413-random-axis-id-todo.md
  • packages/frontend/src/app/spreadsheet/yorkie-worksheet-axis.ts
  • packages/sheets/src/model/workbook/worksheet-grid.ts
  • packages/sheets/src/model/workbook/worksheet-record.ts
  • packages/sheets/test/model/worksheet-record.test.ts

Comment on lines +80 to +81
export function createWorksheetAxisId(prefix: 'r' | 'c'): string {
const bytes = crypto.getRandomValues(new Uint8Array(AXIS_ID_LENGTH));

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's a Node.js version constraint and where this module is imported
echo "=== Checking Node.js version in package.json files ==="
fd -t f 'package.json' --exec grep -l 'engines' {} \; | xargs -I {} sh -c 'echo "--- {} ---" && cat {} | jq ".engines"'

echo ""
echo "=== Checking if worksheet-record.ts is used in backend/server code ==="
rg -l 'worksheet-record' --type ts -g '**/backend/**' -g '**/server/**'

Repository: wafflebase/wafflebase

Length of output: 248


🏁 Script executed:

#!/bin/bash
# Check all package.json files for engine constraints
echo "=== All package.json engine constraints ==="
fd -t f 'package.json' --exec sh -c 'cat {} | jq -r "select(.engines != null) | \"\({}\): \(.engines)\"" 2>/dev/null' \;

echo ""
echo "=== Root package.json ==="
cat package.json | jq '.engines // "No engine constraint"'

echo ""
echo "=== Check worksheet-record imports in test files ==="
rg -l 'worksheet-record|createWorksheetAxisId' --type ts -g '**/*.test.ts' -g '**/*.spec.ts' | head -20

echo ""
echo "=== Check where createWorksheetAxisId is used ==="
rg 'createWorksheetAxisId' --type ts -B 2 -A 2 | head -50

Repository: wafflebase/wafflebase

Length of output: 3585


🏁 Script executed:

#!/bin/bash
# Check how crypto is imported in worksheet-record.ts
echo "=== Check crypto import in worksheet-record.ts ==="
head -20 packages/sheets/src/model/workbook/worksheet-record.ts

echo ""
echo "=== Check if crypto is imported at all ==="
rg 'import.*crypto' packages/sheets/src/model/workbook/worksheet-record.ts

echo ""
echo "=== Check vitest/test configuration ==="
fd -t f 'vitest.config.*' -o 'vite.config.*' --exec cat {} \; | head -100

echo ""
echo "=== Check packages.json in packages/sheets ==="
cat packages/sheets/package.json | jq '{engines, devDependencies: .devDependencies | keys | map(select(contains("vitest") or contains("node")))}'

Repository: wafflebase/wafflebase

Length of output: 940


🏁 Script executed:

#!/bin/bash
# Read the full worksheet-record.ts file to find crypto reference
echo "=== Full worksheet-record.ts file ==="
cat packages/sheets/src/model/workbook/worksheet-record.ts

echo ""
echo "=== Check for any build/polyfill config ==="
fd -type f 'tsconfig.json' 'vite.config.ts' 'webpack.config.ts' --exec sh -c 'echo "--- {} ---" && cat {}' \; | head -150

Repository: wafflebase/wafflebase

Length of output: 2834


🏁 Script executed:

#!/bin/bash
# Check packages/sheets context and purpose
echo "=== packages/sheets README ==="
cat packages/sheets/README.md 2>/dev/null || echo "No README found"

echo ""
echo "=== packages/sheets package.json (name, description, main) ==="
cat packages/sheets/package.json | jq '{name, description, main, browser, exports}'

echo ""
echo "=== Check if sheets is exported as a browser library ==="
cat packages/sheets/tsconfig.json 2>/dev/null | jq '.compilerOptions.lib, .compilerOptions.target' || echo "No tsconfig or lib config found"

Repository: wafflebase/wafflebase

Length of output: 4016


Consider adding an engines constraint to packages/sheets for clarity.

crypto.getRandomValues() relies on the global crypto object, which is standard in browsers and available in Node.js 15+. Since the library lacks an explicit engines field in package.json, add "engines": { "node": ">=15" } to document the minimum Node.js version requirement. This also affects the test environment (Vitest requires Node.js 18+) and any downstream consumers who may bundle this code.

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

In `@packages/sheets/src/model/workbook/worksheet-record.ts` around lines 80 - 81,
Add an explicit engines constraint to the package.json for the packages/sheets
package so consumers and CI know the minimum Node version; update the
packages/sheets package.json to include an "engines" field (e.g., "node": ">=18"
to satisfy Vitest and modern crypto.getRandomValues availability) and commit the
change so tests and downstream consumers are aware of the runtime requirement.

Concurrent row/column insertion by two clients could read the same
nextRowId counter value and generate identical axis IDs (e.g. both
create "r93"), causing a single row ID to appear at two positions
in rowOrder. This was observed in production where editing row 15
also affected row 30 because both mapped to "r93".

Replace the shared sequential counter with 4-char base36 random IDs
generated via crypto.getRandomValues (1.6M+ combinations), making
collisions structurally impossible regardless of concurrency.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@codecov

codecov Bot commented Apr 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ckages/sheets/src/model/workbook/worksheet-grid.ts 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@hackerwins
hackerwins merged commit 9946404 into main Apr 13, 2026
4 checks passed
@hackerwins
hackerwins deleted the fix/random-axis-id branch April 13, 2026 06:55
hackerwins added a commit that referenced this pull request Apr 19, 2026
- random-axis-id (#127)
- axis-id-selection (#130)
- docs-image-editing Phase 1 (#121, #123)
- nested-tables (#128, #129)
- docs-mobile-toolbar (#132)
- release-v0.3.3 (#131)
- docs-image-editing lessons
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.

1 participant