Skip to content

fix: add null checks for foreignRecordMap in link field update methods#2667

Open
dkindlund wants to merge 1 commit intoteableio:developfrom
dkindlund:fix/link-foreign-record-null-check
Open

fix: add null checks for foreignRecordMap in link field update methods#2667
dkindlund wants to merge 1 commit intoteableio:developfrom
dkindlund:fix/link-foreign-record-null-check

Conversation

@dkindlund
Copy link
Contributor

Bug Description

When a desync exists between the JSONB cell values and the junction table for many-to-many (or other) link fields, updating the link field via the API crashes with:

TypeError: Cannot read properties of undefined (reading '<fieldId>')
    at LinkService.updateForeignCellForManyMany
    at LinkService.updateLinkRecord
    at LinkService.getDerivateByCellContexts

This happens because foreignRecordMap is built from JSONB cell values (via getRecordMapStruct), but toDelete/toAdd IDs come from the junction table (via getForeignKeys). When the JSONB column is empty or out of sync with the junction table, records in toDelete are not loaded into foreignRecordMap, causing the undefined property access.

The toAdd branches already have null checks (throwing CustomHttpException), but the toDelete/oldKey branches do not — creating an asymmetry that crashes the server with an unhandled TypeError.

Steps to Reproduce

  1. Create two tables with a ManyMany link field between them
  2. Create a desync between the JSONB column and the junction table (this can happen naturally under concurrent load due to the read-modify-write race condition fixed in commit 77a8291d2)
  3. Attempt to PATCH the record's link field value via the API
  4. The server crashes with TypeError: Cannot read properties of undefined

How the desync occurs in production:
Under concurrent load, multiple transactions can read the same JSONB snapshot, each compute their own version, and the last writer silently overwrites the others' changes (PostgreSQL READ COMMITTED default). Over time, the JSONB column falls behind the junction table. The FOR UPDATE locking fix in commit 77a8291d2 prevents new desyncs, but does not fix existing ones — and this null check bug makes existing desync'd records permanently unrecoverable via the API.

Expected Behavior

Updating a link field on a record with a JSONB/junction desync should not crash the server. The operation should either:

  • Skip the missing foreign records gracefully (as this fix does), or
  • Load the missing records from the database before processing

What This Fix Does

Adds null guards before accessing foreignRecordMap[foreignRecordId] in the deletion/update branches of all four link field update methods:

  • updateForeignCellForManyMany (toDelete loop)
  • updateForeignCellForManyOne (oldKey loop)
  • updateForeignCellForOneMany (toDelete loop + toAdd loop)
  • updateForeignCellForOneOne (oldKey loop + newKey assignment)

Client Information

  • OS: Linux (Docker container on Google Cloud Run)
  • Database: PostgreSQL 17 (Google Cloud SQL)
  • Teable Version: Enterprise edition (deployed from main:latest circa Oct 2025)

Platform

  • Docker standalone (Google Cloud Run)

Additional Context

  • The root cause race condition was fixed upstream in commit 77a8291d2 ("fix: fix link concurrent update"), which added FOR UPDATE row-level locking. However, that fix only prevents new desyncs — it does not help with existing desync'd records, which remain permanently stuck because this null check bug prevents any API-based fix.
  • We encountered this in production with a "Microsoft Windows" technology record that had 0 entries in its JSONB column but 602 rows in the junction table. The record could not be updated (TypeError crash) or deleted (FK constraint violation), requiring direct database-level intervention to resolve.
  • The fix includes E2E tests covering all four relationship types (ManyMany, ManyOne, OneMany, OneOne) exercising the toDelete/oldKey code paths.

When a desync exists between the JSONB cell values and the junction table
(e.g., due to a prior concurrent update race condition), the
foreignRecordMap may not contain all record IDs that appear in the
toDelete/toAdd lists. This causes a TypeError crash:

  TypeError: Cannot read properties of undefined (reading '<fieldId>')

The foreignRecordMap is built from JSONB cell values (via
getRecordMapStruct), but toDelete/toAdd IDs come from the junction table
(via getForeignKeys). When the JSONB is empty but the junction table has
rows, records in toDelete are not loaded into foreignRecordMap.

This fix adds null guards before accessing foreignRecordMap entries in
the deletion/update branches of all four link field update methods:

- updateForeignCellForManyMany
- updateForeignCellForManyOne
- updateForeignCellForOneMany
- updateForeignCellForOneOne

The addition branches already have null checks (throwing
CustomHttpException), but the deletion branches did not, creating an
asymmetry that could crash the server.

Co-Authored-By: Claude Opus 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.

1 participant