Skip to content

Use fractional indexing for the positions of the todo items - #20

Merged
fulopkovacs merged 14 commits into
mainfrom
use-fractional-indexing
Dec 20, 2025
Merged

Use fractional indexing for the positions of the todo items#20
fulopkovacs merged 14 commits into
mainfrom
use-fractional-indexing

Conversation

@fulopkovacs

@fulopkovacs fulopkovacs commented Dec 19, 2025

Copy link
Copy Markdown
Owner

Previously we tracked the positions of the todo items in a single json file per project. This led to errors, especially when the network latency was high and the items were being reordered fast.

String-based fractional indexing should solve this issue.

Summary by CodeRabbit

  • New Features

    • Batch update support for applying changes to multiple todo items at once
  • Improvements

    • Smoother, more responsive drag-and-drop with more reliable ordering and position persistence
    • Faster/new-item insertion at the front of boards for quicker task creation
    • Floating window: improved open/close animation and resizable behavior
  • Bug Fixes

    • Long error messages now scroll to avoid layout overflow

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 19, 2025

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly and accurately describes the primary change across the changeset—migrating from a JSON-based position storage system to string-based fractional indexing for todo item positions.

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b84d95 and 38052c0.

📒 Files selected for processing (2)
  • src/components/TodoBoards.tsx (12 hunks)
  • src/routes/__root.tsx (1 hunks)

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

@github-actions

github-actions Bot commented Dec 19, 2025

Copy link
Copy Markdown

🚀 Preview Deployment

Status: ✅ Ready!

Preview URL: Open Preview

Commit: 38052c0827d9a541656b55cb39d2126785a854fa

Built and deployed successfully

@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 (4)
drizzle/migrations/0000_absurd_polaris.sql (1)

14-14: Critical: Remove obsolete item_positions_in_the_project field from projects table.

The migration SQL retains the item_positions_in_the_project field in the projects table, but src/db/schema.ts removes it. This creates a critical schema/migration mismatch that will cause runtime errors when the ORM tries to query or insert projects.

Since the PR transitions to per-item position tracking using the new position field in todo_items, this JSON field is no longer needed.

🔎 Proposed fix
 CREATE TABLE "projects" (
 	"id" text PRIMARY KEY NOT NULL,
 	"name" text NOT NULL,
 	"description" text NOT NULL,
 	"created_at" date NOT NULL,
-	"item_positions_in_the_project" json DEFAULT '{}'::json NOT NULL,
 	CONSTRAINT "projects_name_unique" UNIQUE("name")
 );
src/db/schema.ts (1)

1-1: Fix formatting issues.

The pipeline indicates formatting issues in this file.

#!/bin/bash
# Run formatter on schema.ts
pnpm format --write src/db/schema.ts
src/db/seed.ts (1)

297-314: Critical: Remove obsolete itemPositionsInTheProject construction.

The code constructs itemPositionsInTheProject and includes it in the returned projects, but this field was removed from ProjectRecord in src/db/schema.ts. This will cause a TypeScript compilation error and runtime issues when inserting projects.

Since position tracking now occurs via the per-item position field, this entire code block should be removed.

🔎 Proposed fix
-  const projectsWithPositions: ProjectRecord[] = mockProjects.map((project) => {
-    const projectBoards = mockBoards.filter(
-      (board) => board.projectId === project.id,
-    );
-    const itemPositionsInTheProject: Record<string, string[]> = {};
-
-    for (const board of projectBoards) {
-      const itemsInBoard = mockTodoItems
-        .filter((item) => item.boardId === board.id)
-        .map((item) => item.id);
-      itemPositionsInTheProject[board.id] = itemsInBoard;
-    }
-
-    return {
-      ...project,
-      itemPositionsInTheProject,
-    };
-  });
+  const projectsWithPositions: ProjectRecord[] = mockProjects.map(
+    ({ todoItemsBaseArr, ...project }) => project,
+  );

   return {
     mockUsers,
     mockBoards,
     mockTodoItems,
     mockProjects: projectsWithPositions,
   };
src/local-api/api.todo-items.ts (1)

7-35: Remove redundant Omit type on line 35.

Line 35 defines newTodoItemData as Omit<z.infer<typeof todoItemCreateData>, "projectId">, but projectId is not present in the todoItemCreateData schema (lines 7-14). The Omit is redundant and misleading.

🔎 Proposed fix
-    let newTodoItemData: Omit<z.infer<typeof todoItemCreateData>, "projectId">;
+    let newTodoItemData: z.infer<typeof todoItemCreateData>;
🧹 Nitpick comments (2)
src/db/seed.ts (1)

28-28: Address TODO: Differentiate between projects and boards.

The TODO indicates a need to clarify the distinction between projects and boards in the seeding logic. This may involve refactoring the data model or generator structure.

Would you like me to help implement this clarification, or should I open a tracking issue for future work?

src/components/TodoBoards.tsx (1)

302-335: Consider renaming maxTodoItemsBoard for clarity.

The query name maxTodoItemsBoard is misleading—it fetches all todo items for the project, not just the maximum or a subset. Consider renaming to something like projectTodoItemsWithBoard or allTodoItemsInProject for better clarity.

The query implementation itself is correct, with proper joins, filtering, and lexical sorting for fractional indexing.

🔎 Suggested naming improvement
-  const { data: maxTodoItemsBoard } = useLiveQuery(
+  const { data: projectTodoItems } = useLiveQuery(
     (q) =>
       q
         .from({

Then update all references to maxTodoItemsBoard throughout the file to use the new name.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 43c3a9a and 89d5170.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • TODO.md (1 hunks)
  • drizzle/migrations/0000_absurd_polaris.sql (1 hunks)
  • drizzle/migrations/meta/0000_snapshot.json (2 hunks)
  • drizzle/migrations/meta/_journal.json (1 hunks)
  • package.json (1 hunks)
  • src/collections/todoItems.ts (1 hunks)
  • src/components/CreateOrEditTodoItems.tsx (2 hunks)
  • src/components/TodoBoards.tsx (11 hunks)
  • src/db/migrations.json (1 hunks)
  • src/db/schema.ts (1 hunks)
  • src/db/seed.ts (4 hunks)
  • src/local-api/api.todo-items.ts (5 hunks)
  • tsconfig.json (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-19T21:20:23.301Z
Learnt from: CR
Repo: fulopkovacs/trytanstackdb.com PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-19T21:20:23.301Z
Learning: Use `pnpm db.generate` to generate database migrations

Applied to files:

  • src/db/seed.ts
🧬 Code graph analysis (2)
src/components/CreateOrEditTodoItems.tsx (1)
src/collections/todoItems.ts (1)
  • todoItemsCollection (60-95)
src/components/TodoBoards.tsx (4)
src/components/CreateOrEditTodoItems.tsx (1)
  • CreateOrEditTodoItems (19-143)
src/collections/todoItems.ts (1)
  • todoItemsCollection (60-95)
src/collections/boards.ts (1)
  • boardCollection (17-24)
src/collections/projects.ts (1)
  • projectsCollection (16-41)
🪛 GitHub Actions: CI/CD
src/db/migrations.json

[error] 1-1: Formatter would have printed content; formatting issues detected in migrations.json.

src/db/schema.ts

[error] 1-1: Formatter would have printed content; formatting issues detected in schema.ts.

🪛 LanguageTool
TODO.md

[style] ~11-~11: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...? - [ ] (feature) query-driven sync - maybe the todos -> per board - [ ] `robots.tx...

(REP_MAYBE)


[style] ~15-~15: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...ly) - [ ] (feature) paced mutations - maybe editing the todos - [ ] Make the root e...

(REP_MAYBE)

🔇 Additional comments (16)
drizzle/migrations/0000_absurd_polaris.sql (1)

30-31: LGTM: Position field correctly added.

The new position field is properly defined as NOT NULL and placed logically after the priority field, supporting the fractional indexing implementation.

drizzle/migrations/meta/_journal.json (1)

8-9: LGTM: Migration journal correctly updated.

The timestamp and tag reflect the new migration version for the position field addition.

src/collections/todoItems.ts (1)

69-71: LGTM: Insert payload correctly updated.

The change to pass newTodoItem directly aligns with the removal of projectId from the creation flow and the new position-based ordering system.

src/db/schema.ts (2)

62-62: LGTM: Position field correctly added to schema.

The new position field with notNull() constraint properly supports the fractional indexing implementation. The schema change aligns with the migration's intent to add per-item position tracking.


32-37: Verify removal of itemPositionsInTheProject field is reflected in migrations.

The schema correctly removes itemPositionsInTheProject from the projects table as part of the position-based ordering refactor. However, the migration SQL files still contain this field, creating a critical mismatch. Ensure the migration is updated to match this schema.

src/db/seed.ts (2)

29-45: LGTM: Fractional index generator correctly implemented.

The per-board position generator using generateKeyBetween is well-designed. The lazy initialization pattern via the boardGenerators map ensures each board maintains its own independent position sequence, which is essential for correct fractional indexing behavior.


54-168: LGTM: Seed data correctly uses position-based ordering.

All todo items now receive deterministic positions via getNextPosition, properly segregated by board. The use of board-specific generators (e.g., "board-1-todo", "board-2-in-progress", "large-project-board-Done") ensures correct ordering within each board's context.

package.json (1)

54-54: No action required.

The fractional-indexing package version ^3.2.0 exists and is the latest stable release with no known security vulnerabilities.

src/components/CreateOrEditTodoItems.tsx (3)

1-25: LGTM on component refactoring.

The removal of projectId from props and the addition of fractional-indexing imports align well with the position-based ordering approach. The component signature is cleaner and more focused.


44-49: Correction: JavaScript < operator IS lexicographical - no issue here.

After verification, JavaScript's comparison operators (<, >) perform lexicographical comparison based on Unicode code points, not locale-based comparison. The < and > operators compare strings using Unicode encodings, which is exactly what fractional indexing requires.

The sorting on line 46 using a.position < b.position is correct and consistent with the stringSort: "lexical" approach used in TodoBoards.tsx. Both methods produce the same lexicographical (byte-order) sort needed for fractional indexing.

Please disregard my earlier concern about sorting inconsistency.


44-49: No changes needed. The code correctly uses native JavaScript string comparison (< operator) for fractional indexing, which performs lexicographical (Unicode code point) ordering. This is the recommended approach per the fractional-indexing library documentation and is appropriate for position sorting. Using localeCompare() instead would actually break fractional indexing due to case-insensitive collation.

Likely an incorrect or invalid review comment.

src/components/TodoBoards.tsx (4)

42-62: LGTM on findPrevItem helper.

The helper function correctly identifies the previous item in the same board. The generic type constraints ensure type safety, and the logic properly handles edge cases (first item, different boards).


158-173: LGTM on Board component query with lexical sorting.

The query correctly uses stringSort: "lexical" for fractional indexing. The comment explaining the difference between lexical and locale-based sorting is excellent and helps prevent future bugs.


177-207: LGTM on drop indicator calculation.

The drop indicator logic correctly handles all drag-and-drop scenarios and properly avoids showing indicators when the item wouldn't actually move. The useMemo dependencies are complete.


341-415: LGTM on drag-and-drop implementation.

The handleDragEnd logic correctly handles all three drag-and-drop scenarios:

  1. Dropping on an empty board or at the end
  2. Reordering within the same board
  3. Moving to a different board

The fractional indexing calculations are correct:

  • generateKeyBetween(lastPosition, null) for appending
  • generateKeyBetween(null, null) for empty boards
  • generateKeyBetween(prev?.position ?? null, overTodoItem.position) for inserting between items

The use of findPrevItem with the sorted maxTodoItemsBoard data ensures correct predecessor identification.

drizzle/migrations/meta/0000_snapshot.json (1)

188-193: No changes needed. The migration correctly defines the position column as part of the initial table creation with notNull: true and no default value, which is appropriate for CREATE TABLE statements since the table contains no existing data.

Likely an incorrect or invalid review comment.

Comment thread src/db/migrations.json Outdated
Comment thread tsconfig.json Outdated

@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: 0

🧹 Nitpick comments (1)
src/db/schema.ts (1)

56-56: Add a compound index on (boardId, position) for query performance.

Position-based ordering is used in queries that first filter by boardId then sort by position (e.g., TodoBoards.tsx lines 162, 323). A compound index on these columns in this order will allow the database to skip explicit sort operations for these common queries.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 89d5170 and 5640610.

📒 Files selected for processing (5)
  • drizzle/migrations/0000_volatile_doctor_strange.sql (1 hunks)
  • drizzle/migrations/meta/0000_snapshot.json (2 hunks)
  • drizzle/migrations/meta/_journal.json (1 hunks)
  • src/db/migrations.json (1 hunks)
  • src/db/schema.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/db/migrations.json
  • drizzle/migrations/meta/_journal.json
🔇 Additional comments (4)
src/db/schema.ts (1)

1-1: LGTM: Import cleanup aligns with schema changes.

The removal of json from imports is appropriate since itemPositionsInTheProject was removed from the projects table.

drizzle/migrations/meta/0000_snapshot.json (2)

181-186: Verify consistency with NOT NULL position column.

The position column is defined as notNull: true without a default value, consistent with the migration file. This requires that all position values are provided at insert time.

Ensure that:

  • All code paths creating todo items generate position values using fractional-indexing
  • No legacy code attempts to create todo items without position
#!/bin/bash
# Find all locations where todo items are inserted
ast-grep --pattern 'db.insert($$$).values({
  $$$
})'

# Search for TodoItem creation or insertion patterns
rg -n "insert.*todoItem|create.*TodoItem|new.*TodoItem" -g '*.ts' -g '*.tsx' -C5

2-2: No critical issue: This is a greenfield project without existing deployments.

The migration ID change is not problematic here. Your project is in ALPHA stage with a local PGlite database (development-only), no deployment configurations, and this is the first (0000) migration ever created. No database instance has previously run the old migration ID 7f0dad39-3f27-4237-a783-43c6b501d2fc, and no migration tracking table contains records from it.

As noted in the original review: "If no production/staging databases exist yet, this is acceptable" — which is exactly the case here.

Likely an incorrect or invalid review comment.

drizzle/migrations/0000_volatile_doctor_strange.sql (1)

29-30: The migration concern is not applicable—this is an initial table creation.

The position column is part of a CREATE TABLE statement for a new schema, not an ALTER TABLE operation on existing data. Since migration 0000 creates the todo_items table from scratch, no existing rows need a default value. The NOT NULL constraint is appropriate for initial schema creation.

Likely an incorrect or invalid review comment.

@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: 0

🧹 Nitpick comments (2)
src/local-api/api.batch.todo-items.ts (1)

39-46: Consider excluding id from the update operation.

Including id in the .set() call is unnecessary since it's already used in the .where() clause. While Drizzle likely ignores it, explicitly excluding it improves clarity.

🔎 Proposed refactor
     await db.transaction(async (tx) => {
       for (const todoItemData of updatedData) {
+        const { id, ...updateFields } = todoItemData;
         await tx
           .update(todoItemsTable)
-          .set(todoItemData)
+          .set(updateFields)
-          .where(eq(todoItemsTable.id, todoItemData.id));
+          .where(eq(todoItemsTable.id, id));
       }
     });
src/components/TodoBoards.tsx (1)

292-334: Consider reducing the debounce wait time.

A 3-second debounce might feel sluggish for drag-and-drop interactions. While optimistic updates provide immediate visual feedback, users may expect changes to persist more quickly. Consider reducing to 1000-1500ms for better UX, unless the 3-second delay is intentional for rate-limiting or batching purposes.

🔎 Suggested adjustment
-    strategy: debounceStrategy({ wait: 3000 }),
+    strategy: debounceStrategy({ wait: 1500 }),
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6226e51 and 449e2c4.

📒 Files selected for processing (5)
  • src/collections/todoItems.ts (2 hunks)
  • src/components/TodoBoards.tsx (12 hunks)
  • src/local-api/api.batch.todo-items.ts (1 hunks)
  • src/local-api/api.todo-items.ts (4 hunks)
  • src/local-api/index.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/local-api/api.batch.todo-items.ts (4)
src/local-api/api.todo-items.ts (1)
  • todoItemUpdateData (18-25)
src/db/index.ts (1)
  • db (10-12)
src/db/schema.ts (1)
  • todoItemsTable (47-57)
src/local-api/helpers.ts (2)
  • json (29-43)
  • APIRouteHandler (15-17)
src/components/TodoBoards.tsx (4)
src/components/CreateOrEditTodoItems.tsx (1)
  • CreateOrEditTodoItems (19-143)
src/db/schema.ts (1)
  • TodoItemRecord (59-59)
src/collections/todoItems.ts (2)
  • todoItemsCollection (80-115)
  • batchUpdateTodoItem (38-56)
src/collections/boards.ts (1)
  • boardCollection (17-24)
src/collections/todoItems.ts (1)
src/db/schema.ts (1)
  • TodoItemRecord (59-59)
🔇 Additional comments (9)
src/local-api/index.ts (1)

1-1: LGTM! Clean integration of the batch API route.

The new batch endpoint is properly imported and exposed, enabling bulk updates for todo items.

Also applies to: 11-11

src/collections/todoItems.ts (2)

38-56: LGTM! Batch update function is well-structured.

The new batchUpdateTodoItem function properly handles batch updates with appropriate error handling.


90-90: LGTM! Simplified payload aligns with schema changes.

Passing newTodoItem directly is correct now that the position field is part of the todo item record.

src/local-api/api.todo-items.ts (2)

13-13: LGTM! Position fields correctly integrated.

The schema now properly includes position for both creation and updates, aligning with the fractional indexing approach.

Also applies to: 24-24


47-48: LGTM! Simplified creation logic.

Removing the projectId extraction and directly using todoItemData is correct given the schema changes.

src/components/TodoBoards.tsx (4)

50-70: LGTM! Well-designed helper with clear constraints.

The findPrevItem function correctly finds the previous item within the same board, with good generic type constraints and clear documentation.


166-181: LGTM! Correct lexical sorting for fractional indexing.

The query properly uses stringSort: "lexical" for fractional index ordering, with an excellent comment explaining why this is necessary.


354-387: LGTM! Comprehensive query with correct ordering.

The joined query properly fetches all necessary fields and applies lexical sorting for fractional indexes. The comment clearly explains the sorting requirements.


393-470: LGTM! Robust drag-and-drop handling.

The handleDragEnd implementation correctly handles three scenarios:

  1. Dropping on an empty column or at the end
  2. Reordering within the same column
  3. Moving to a different column

Position generation using generateKeyBetween with appropriate boundaries is properly implemented.

@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: 0

Caution

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

⚠️ Outside diff range comments (1)
src/components/TodoBoards.tsx (1)

418-420: Critical: Fix method invocation syntax.

Line 418 attempts to access .find on todoItemsCollection.toArray, but toArray appears to be a method based on the related code snippet showing todoItemsCollection.toArray() at line 331. This will cause a runtime TypeError.

🔎 Proposed fix
-      const overTodoItem = todoItemsCollection.toArray.find(
+      const overTodoItem = todoItemsCollection.toArray().find(
         (item) => item.id === over.id,
       );
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 449e2c4 and 39b67fe.

📒 Files selected for processing (1)
  • src/components/TodoBoards.tsx (12 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/TodoBoards.tsx (2)
src/components/CreateOrEditTodoItems.tsx (1)
  • CreateOrEditTodoItems (19-143)
src/collections/todoItems.ts (2)
  • todoItemsCollection (80-115)
  • batchUpdateTodoItem (38-56)
🔇 Additional comments (11)
src/components/TodoBoards.tsx (11)

15-30: LGTM! Imports correctly support fractional indexing.

The new imports for usePacedMutations, debounceStrategy, and generateKeyBetween are appropriately added to support the fractional indexing feature with debounced batch updates.


50-70: LGTM! Helper function correctly identifies the previous item.

The findPrevItem function properly handles edge cases (first item, different board) and uses appropriate type constraints to ensure type safety.


166-181: LGTM! Correct use of lexical sorting for fractional indexes.

The query properly uses stringSort: "lexical" which is essential for maintaining correct ordering with fractional indexing. The explanatory comment is helpful.


185-215: LGTM! Drop indicator logic is correct.

The dropIndex calculation properly handles both dropping over specific items and dropping on the board itself (empty column or end position).


238-269: LGTM! Rendering logic correctly uses live todoItems.

The Board component now consistently renders based on the live-queried todoItems array, properly integrating with the fractional indexing system.


292-334: Verify the 3-second debounce timing.

The implementation correctly applies optimistic updates immediately and batches backend persistence. However, a 3-second debounce is relatively long—users performing rapid successive drags might experience a delay before changes are persisted. Additionally, there's no explicit error handling in mutationFn; if batchUpdateTodoItem fails, the optimistic updates may remain inconsistent until the next refetch.

Consider:

  1. Testing the debounce timing under typical usage patterns
  2. Adding error handling that reverts optimistic updates on failure:
mutationFn: async ({ transaction }) => {
  const mutations = transaction.mutations;
  const updates = mutations.reduce(/* ... */);
  
  try {
    await batchUpdateTodoItem({ data: updates });
    await todoItemsCollection.utils.refetch();
  } catch (error) {
    console.error("Failed to persist position updates:", error);
    // Refetch will restore backend state
    await todoItemsCollection.utils.refetch();
    throw error;
  }
},

401-416: LGTM! Correct positioning logic for drops on empty/end.

The logic properly handles dropping on a board by generating a position after the last item (or the first position if empty) using generateKeyBetween.


422-442: LGTM! Same-column reordering logic is correct.

The code properly computes the new position between the previous item and the drop target using findPrevItem and generateKeyBetween.


443-465: LGTM! Cross-board move logic is correct.

The code properly handles moving items to a different board by computing the correct position and updating both boardId and position.


480-495: LGTM! TodoBoards rendering structure is correct.

The component properly sets up the DnD context and renders boards with the correct props.


354-387: Consider using leftJoin instead of innerJoin if orphaned todo items should be displayed.

The query uses innerJoin for board and project associations, meaning todo items with invalid or missing boardId values—or boards with invalid projectId values—will be silently excluded from results. If the data schema enforces foreign key constraints with cascade delete or no-action rules, this is safe; orphaned records shouldn't exist. However, if constraints are not enforced, items could be hidden from the UI unintentionally. Verify whether the schema requires valid board/project references and update the join type accordingly.

@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

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66741bc and 7b84d95.

📒 Files selected for processing (6)
  • drizzle/migrations/0000_outstanding_silvermane.sql (2 hunks)
  • drizzle/migrations/meta/0000_snapshot.json (2 hunks)
  • drizzle/migrations/meta/_journal.json (1 hunks)
  • src/components/TodoBoards.tsx (12 hunks)
  • src/db/migrations.json (1 hunks)
  • src/db/schema.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-19T21:20:23.301Z
Learnt from: CR
Repo: fulopkovacs/trytanstackdb.com PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-19T21:20:23.301Z
Learning: Use `pnpm db.generate` to generate database migrations

Applied to files:

  • src/db/migrations.json
🧬 Code graph analysis (2)
src/db/migrations.json (2)
src/components/CreateOrEditTodoItems.tsx (2)
  • project (39-43)
  • newItemData (32-44)
src/db/seed.ts (2)
  • project (261-278)
  • item (179-184)
src/components/TodoBoards.tsx (5)
src/components/CreateOrEditTodoItems.tsx (1)
  • CreateOrEditTodoItems (19-143)
src/db/schema.ts (1)
  • TodoItemRecord (72-72)
src/collections/todoItems.ts (2)
  • todoItemsCollection (80-115)
  • batchUpdateTodoItem (38-56)
src/collections/boards.ts (1)
  • boardCollection (17-24)
src/collections/projects.ts (1)
  • projectsCollection (16-41)
🔇 Additional comments (9)
drizzle/migrations/meta/_journal.json (1)

8-9: LGTM!

Migration metadata correctly updated with new timestamp and tag name for the fractional indexing migration.

drizzle/migrations/0000_outstanding_silvermane.sql (1)

43-43: LGTM!

The composite index on (board_id, position) is essential for efficiently querying and sorting todo items by their fractional position within each board. This aligns perfectly with the PR objective of using fractional indexing for position management.

src/db/migrations.json (1)

1-17: LGTM!

The compiled migration correctly reflects the schema changes: item_positions_in_the_project removed from projects table, position field added to todo_items, and the composite index on (board_id, position) created. Previous formatting and schema mismatch issues have been addressed.

src/db/schema.ts (1)

54-70: LGTM!

The schema refactoring correctly implements fractional indexing by:

  • Adding the required position text field
  • Using the three-argument pgTable signature to define a composite index on (boardId, position)
  • Ensuring efficient queries for position-based sorting within boards

The schema changes align perfectly with the migration SQL.

src/components/TodoBoards.tsx (4)

50-70: LGTM!

The findPrevItem helper correctly identifies the previous item within the same board from a position-sorted array. The boardId check ensures cross-board boundaries are respected when calculating new fractional positions.


166-181: LGTM!

The query correctly implements lexical string sorting for fractional index positions. This is crucial because fractional indexing relies on lexical comparison (e.g., "Zz" < "a0") rather than locale-based sorting. The inline comment helpfully explains this distinction.


354-387: LGTM!

The cross-table query correctly fetches all todo items for a project with their positions, ordered by board and position using lexical sorting. This structure supports the findPrevItem helper by maintaining items grouped by board and sorted by fractional position within each group.


393-470: LGTM!

The drag-and-drop logic correctly implements fractional indexing for all scenarios:

  1. Drop on empty/end of column: Uses generateKeyBetween(lastPosition, null) to append after last item
  2. Reorder within column: Uses generateKeyBetween(prev, over) to insert between adjacent items
  3. Move to another column: Uses generateKeyBetween(prev, over) with new boardId to position correctly in target board

The logic properly handles edge cases like dropping on empty columns and maintains correct ordering through lexical string comparison.

drizzle/migrations/meta/0000_snapshot.json (1)

181-209: LGTM!

The schema snapshot correctly documents the addition of the position field and the composite index on (board_id, position). The index configuration (btree, ascending, nulls last) is appropriate for efficient range queries on fractional positions within boards.

Comment on lines +292 to +334
// Create paced mutation with 3 second debounce for updating todo positions
const updateTodoPosition = usePacedMutations<
{
itemId: string;
boardId?: string;
newPosition: string;
},
TodoItemRecord
>({
onMutate: ({ itemId, boardId, newPosition }) => {
// Apply optimistic update immediately
todoItemsCollection.update(itemId, (item) => {
if (boardId) {
item.boardId = boardId;
}
item.position = newPosition;
});
},
mutationFn: async ({ transaction }) => {
// Persist all position updates to the backend after debounce
const mutations = transaction.mutations;

const updates = mutations.reduce(
(acc, mutation) => {
const { modified, changes } = mutation;
acc.push({
id: modified.id,
...changes,
});
return acc;
},
[] as (Partial<TodoItemRecord> & { id: string })[],
);

await batchUpdateTodoItem({
data: updates,
});

// Refetch to ensure consistency with backend
await todoItemsCollection.utils.refetch();
},
strategy: debounceStrategy({ wait: 1_500 }),
});

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
# Search for usePacedMutations usage and error handling patterns
echo "=== Searching for usePacedMutations implementation ==="
rg -n "usePacedMutations" --type=ts --type=tsx -B 2 -A 20

echo -e "\n=== Searching for error handling in mutations (onError, catch, try) ==="
rg -n "onError|catch|try" --type=ts --type=tsx | grep -i "mutation\|todo" | head -30

echo -e "\n=== Checking for framework error handling patterns ==="
rg -n "usePacedMutations\|paced.*mutation" --type=ts --type=tsx -B 5 -A 30 | grep -E "(onError|catch|error|rollback)" -i

Repository: fulopkovacs/trytanstackdb.com

Length of output: 357


🌐 Web query:

TanStack React DB usePacedMutations error handling rollback

💡 Result:

Short answer — usePacedMutations supports optimistic updates and automatic rollback. Key points and how to handle errors:

  • onMutate is where you apply the optimistic update immediately. (usePacedMutations docs). [1]
  • The persistence is performed by your mutationFn (run by the chosen strategy). If mutationFn throws/rejects, the transaction is automatically rolled back and the optimistic changes are reverted. You can observe that by awaiting the transaction's isPersisted.promise and catching errors. (Error-handling + Transaction docs). [2][3]
  • Transactions expose rollback() (you can call it manually) and rollbacks cascade for conflicting transactions. Use try/catch around await tx.isPersisted.promise to handle failures and show UI/errors. [3]

Minimal example pattern:

  • onMutate: apply optimistic update.
  • mutationFn: perform network call; throw on failure.
  • call mutate(...) to get tx, then await tx.isPersisted.promise in a try/catch to detect rollback and report error to the user.

References:

  1. usePacedMutations docs. [1]
  2. Error handling guide (automatic rollback when mutation handler throws). [2]
  3. Transaction API (isPersisted.promise, rollback, cascade). [3]

Add error handling for mutation failures and persistence.

The framework automatically rolls back optimistic updates when mutationFn throws, but the code needs explicit error handling. The proper pattern is to catch transaction failures at the call site:

try {
  const tx = updateTodoPosition.mutate({ itemId, boardId, newPosition });
  await tx.isPersisted.promise;
} catch (error) {
  // Handle rollback and show error to user
}

Additionally, the refetch() call in mutationFn could fail silently. Wrap it in a try-catch or ensure the error propagates to trigger rollback if persistence should fail.

@fulopkovacs
fulopkovacs merged commit d7c46f1 into main Dec 20, 2025
1 of 2 checks passed
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