{"record":{"id":"22e4de6e82e3a520","repo":"stride3d/stride","slug":"read-only-object-database","errorCode":null,"errorMessage":"Read-only object database.","messagePattern":"Read-only object database\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"sources/core/Stride.Core.Serialization/Storage/ObjectDatabase.cs","lineNumber":218,"sourceCode":"\n        if (BundleBackend != null)\n            result = result.Union(BundleBackend.EnumerateObjects());\n\n        if (backendRead2 != null)\n            result = result.Union(backendRead2.EnumerateObjects());\n\n        return result;\n    }\n\n    public IEnumerable<ObjectId> EnumerateLooseObjects()\n    {\n        return backendRead1.EnumerateObjects();\n    }\n\n    public void Delete(ObjectId objectId)\n    {\n        if (backendWrite == null)\n            throw new InvalidOperationException(\"Read-only object database.\");\n\n        backendWrite.Delete(objectId);\n    }\n\n    /// <summary>\n    /// Refreshes an object's last-use time for mtime-LRU GC and records it in the session working set\n    /// (see <see cref=\"RetouchWorkingSet\"/>). Best-effort and throttled; only the writable loose-file\n    /// store supports it (bundles/read-only backends are skipped).\n    /// </summary>\n    public void Touch(ObjectId objectId)\n    {\n        touchedObjectIds.TryAdd(objectId, 0);\n        (backendWrite as FileOdbBackend)?.Touch(objectId);\n    }\n\n    /// <summary>\n    /// Re-touches every object hit this session so the working set doesn't age out of the mtime-LRU\n    /// cache while the editor sits idle (no rebuilds to touch it on-hit). Throttled per file.","sourceCodeStart":200,"sourceCodeEnd":236,"githubUrl":"https://github.com/stride3d/stride/blob/96fad776d210c221682aac1ccdf4c79dc046fc38/sources/core/Stride.Core.Serialization/Storage/ObjectDatabase.cs#L200-L236","documentation":"ObjectDatabase.Delete removes an object by id from the write backend. If the database was constructed without a write backend (backendWrite == null), it is read-only and any mutation — including Delete — throws. This is a deliberate guard so read-only mounts (e.g. bundled assets) are never modified.","triggerScenarios":"Calling ObjectDatabase.Delete(objectId) on an ObjectDatabase created/opened without a writable IOdbBackend.","commonSituations":"Deleting cached assets at runtime when the database only has a bundle/read backend; cleanup code running against a read-only APK/asset mount; connecting to the store with read-only flags.","solutions":["Open the ObjectDatabase with a writable backend (backendWrite) before deleting.","Only call Delete in tooling/editor contexts where a write backend exists.","Check for a public writable/readonly indicator before mutating and handle read-only cases gracefully."],"exampleFix":"// before\nobjDatabase.Delete(objectId);\n// after\nif (objDatabase.backendWrite != null) // or use a public IsReadOnly accessor if available\n    objDatabase.Delete(objectId);\nelse\n    logger.Warn(\"Object database is read-only; skipping delete\");","handlingStrategy":"validation","validationCode":"if (database.backendWrite == null)\n    return; // skip mutation on read-only database","typeGuard":null,"tryCatchPattern":"try { database.Delete(objectId); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"Read-only object database\")) {\n    logger.Warn($\"Skipping delete of {objectId}: database is read-only\");\n}","preventionTips":["Know your database mount mode (read-only bundles vs writable store)","Perform deletions in editor/tooling contexts only","Centralize mutations behind a helper that checks writability first"],"tags":["serialization","object-database","read-only"],"backgroundTag":"unsupported-operation","analyzedSha":"96fad776d210c221682aac1ccdf4c79dc046fc38","analyzedAt":"2026-09-14T02:59:31.279Z","contentChangedAt":"2026-09-14T02:59:31.279Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}