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 used.

What it means

YJS-specific schema validator UsePreservedFields rejects table definitions that declare a field named in PRESERVED_FIELDS (currently '$$DELETED'). The YJS-backed storage uses '$$DELETED' as an internal tombstone marker for soft-deleted rows, so user schemas may not collide with it.

Source

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

import type { TableSchemaValidator } from './types';

const PRESERVED_FIELDS = ['$$DELETED'];

interface DataValidator {
  validate(tableName: string, data: any): void;
}

export const yjsTableSchemaValidators: Record<string, TableSchemaValidator> = {
  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. Rename the field to anything not in the reserved list (e.g. 'deleted', 'isDeleted').
  2. If you need soft-delete semantics, rely on the framework's delete machinery instead of re-declaring the tombstone column.
  3. When field names are dynamic, filter out PRESERVED_FIELDS before building the table definition.

Example fix

// before
export const Doc = t.table('doc', {
  id: t.string().primaryKey(),
  $$DELETED: t.boolean(),
});

// after
export const Doc = t.table('doc', {
  id: t.string().primaryKey(),
  deleted: t.boolean().optional(),
});
Defensive patterns

Strategy: validation

Validate before calling

const RESERVED = ['$$DELETED'];
const bad = Object.keys(fields).filter(k => RESERVED.includes(k));
if (bad.length) throw new Error(`reserved field names: ${bad.join(', ')}`);

Prevention

When it happens

Trigger: Declaring a table field literally named '$$DELETED' in a table stored via the YJS adapter; programmatically generating field names from external data that happens to include the reserved one.

Common situations: Very rare; occurs when porting a schema that used a '$$'-prefixed convention, or when field names come from untrusted/dynamic input.

Related errors


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