toeverything/AFFiNE · error · Error

[Table(${tableName})]: Field '${name}' is reserved keyword a

Error message

[Table(${tableName})]: Field '${name}' is reserved keyword and can't be set.

What it means

YJS data validator SetPreservedFields throws when an entity payload tries to set the reserved '$$DELETED' key (any value other than undefined). Unlike error 708 (schema definition), this fires at write time: the tombstone flag is managed exclusively by the framework's delete flow and cannot be written through the normal data API.

Source

Thrown at packages/common/infra/src/orm/core/validators/yjs.ts:28

  UsePreservedFields: {
    validate(tableName, table) {
      for (const name in table) {
        if (PRESERVED_FIELDS.includes(name)) {
          throw new Error(
            `[Table(${tableName})]: Field '${name}' is reserved keyword and can't be used.`
          );
        }
      }
    },
  },
};

export const yjsDataValidators: Record<string, DataValidator> = {
  SetPreservedFields: {
    validate(tableName, data) {
      for (const name of PRESERVED_FIELDS) {
        if (data[name] !== undefined) {
          throw new Error(
            `[Table(${tableName})]: Field '${name}' is reserved keyword and can't be set.`
          );
        }
      }
    },
  },
};

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Remove '$$DELETED' from the payload before writing (strip it or use a pick of real schema fields).
  2. Use the table's delete API to soft-delete rows instead of setting the flag by hand.
  3. If you read raw rows, map them to your entity type and drop internal keys before writing back.

Example fix

// before
await db.doc.set(id, { ...rawRow, title: 'new' }); // rawRow contains $$DELETED

// after
const { ['$$DELETED']: _omit, ...entity } = rawRow;
await db.doc.set(id, { ...entity, title: 'new' });
Defensive patterns

Strategy: validation

Validate before calling

function stripReserved(data: Record<string, unknown>) {
  const clone = { ...data };
  delete clone['$$DELETED'];
  return clone;
}

Type guard

type Entity<T> = Omit<T, '$$DELETED'>;
// Omit the tombstone key when typing round-tripped rows

Try / catch

try { await table.set(id, payload); } catch (e) { if (e instanceof Error && e.message.includes("reserved keyword and can't be set")) { delete payload['$$DELETED']; await table.set(id, payload); } else throw e; }

Prevention

When it happens

Trigger: Passing { ..., '$$DELETED': true } to create/set on a YJS-backed table; spreading a previously-read raw row (which includes the tombstone) back into an update; tests fabricating row objects with the internal flag.

Common situations: Round-tripping internal row objects through user code; copying fixtures that captured the storage-level representation including tombstones.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/7e9b50cff7cfef73. Report an issue: GitHub.