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
- Rename the field to anything not in the reserved list (e.g. 'deleted', 'isDeleted').
- If you need soft-delete semantics, rely on the framework's delete machinery instead of re-declaring the tombstone column.
- 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
- Avoid '$$'-prefixed field names — the framework reserves that namespace
- When generating schemas dynamically from external names, filter against the reserved list
- Prefer plain identifiers for user-facing columns
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
- [Table(${tableName})]: Field '${name}' is reserved keyword a
- EdgelessExportError
- [Table(${table.name})]: Field '${key}' type mismatch. Expect
- [Table(${tableName})]: There should be at least one field ma
- [Table(${tableName})]: There should be only one field marked
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/1f6baa5b0995c632.
Report an issue: GitHub.