{"record":{"id":"1f6baa5b0995c632","repo":"toeverything/AFFiNE","slug":"table-tablename-field-name-is-reserved","errorCode":null,"errorMessage":"[Table(${tableName})]: Field '${name}' is reserved keyword and can't be used.","messagePattern":"\\[Table\\((.+?)\\)\\]: Field '(.+?)' is reserved keyword and can't be used\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/common/infra/src/orm/core/validators/yjs.ts","lineNumber":14,"sourceCode":"import type { TableSchemaValidator } from './types';\n\nconst PRESERVED_FIELDS = ['$$DELETED'];\n\ninterface DataValidator {\n  validate(tableName: string, data: any): void;\n}\n\nexport const yjsTableSchemaValidators: Record<string, TableSchemaValidator> = {\n  UsePreservedFields: {\n    validate(tableName, table) {\n      for (const name in table) {\n        if (PRESERVED_FIELDS.includes(name)) {\n          throw new Error(\n            `[Table(${tableName})]: Field '${name}' is reserved keyword and can't be used.`\n          );\n        }\n      }\n    },\n  },\n};\n\nexport const yjsDataValidators: Record<string, DataValidator> = {\n  SetPreservedFields: {\n    validate(tableName, data) {\n      for (const name of PRESERVED_FIELDS) {\n        if (data[name] !== undefined) {\n          throw new Error(\n            `[Table(${tableName})]: Field '${name}' is reserved keyword and can't be set.`\n          );\n        }\n      }","sourceCodeStart":1,"sourceCodeEnd":32,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/common/infra/src/orm/core/validators/yjs.ts#L1-L32","documentation":"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.","triggerScenarios":"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.","commonSituations":"Very rare; occurs when porting a schema that used a '$$'-prefixed convention, or when field names come from untrusted/dynamic input.","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."],"exampleFix":"// before\nexport const Doc = t.table('doc', {\n  id: t.string().primaryKey(),\n  $$DELETED: t.boolean(),\n});\n\n// after\nexport const Doc = t.table('doc', {\n  id: t.string().primaryKey(),\n  deleted: t.boolean().optional(),\n});","handlingStrategy":"validation","validationCode":"const RESERVED = ['$$DELETED'];\nconst bad = Object.keys(fields).filter(k => RESERVED.includes(k));\nif (bad.length) throw new Error(`reserved field names: ${bad.join(', ')}`);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["orm","yjs","reserved-name","affine"],"backgroundTag":"reserved-field-name","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}