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
- Remove '$$DELETED' from the payload before writing (strip it or use a pick of real schema fields).
- Use the table's delete API to soft-delete rows instead of setting the flag by hand.
- 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
- Use the framework delete API for soft deletes
- Map raw storage rows to clean entity types before writing back
- Never fabricate rows with internal flags in fixtures
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
- [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/7e9b50cff7cfef73.
Report an issue: GitHub.