Eyes pass for variants❓read-only-record-signal. ORMs, embedded
stores, serializers, file systems and object stores; signal shape and
time-of-check pitfalls for each, with evidence pointers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
19 KiB
19 KiB
Read-Only Record Signal: Prior Art Survey
Facts file for Eureka question variants:question:read-only-record-signal: how a
developer learns that a held record is read-only before a write is refused. Its
options are (a) no new API, (b) a live query such as
CultCache.IsWritable(key), and (c) a list beside ForeignRecords. Eyes pass,
2026-10-03. Facts and evidence pointers only; no recommendation.
Verification marks:
- [fetched] the claim was read on the cited page or source during this pass.
- [search] the claim came from a search-result summary of the cited page, not a full read.
- [memory] recalled from the cited source, not re-read in this pass. Treat it as unverified.
Signal shapes used below:
- property: a flag or state on the loaded object itself.
- query: a call on the store, session or registry that answers at call time.
- list: an enumeration of affected items, captured or computed.
- type: the read-only status is a distinct static type.
- exception only: nothing to ask; the write is the first signal.
0. Our design, for comparison
Source: branch hands/variants-c2a (head e9a66876), src/GameCult.Caching/CultCache.cs.
CultDocumentRegistry.Declares(string schemaId)(line 738) isinternal. Its comment: "The one answer to whether a cache may write, relabel or remove a record stored under this id ... Read live, so a type registered later that declares the id claims its records at their next write." [fetched]GetRequired<T>()(line 631) registers lazily, so the set of declared ids can grow after load. [fetched]- The backing store already exposes
ForeignRecords(line 3421), anIReadOnlyList<CultForeignRecord>"the last load found and carries untouched", set at load (SetForeignRecords, line 3599).CultForeignRecord(line 162) carriesKey,SchemaId,SchemaName,StoredAt. [fetched] - The store also has a store-level
public bool IsReadOnly { get; }(line 3419), fixed at construction. [fetched] - Main (
162bd1fc) exposesLastSchemaMigrationReports(CultCache.cs:3136), whose entries carry no record key (per the question text). [fetched for the property; the no-key claim is the question's]
1. ORMs
EF Core: no-tracking queries and keyless entity types
- Signal shape: query on the context, plus a model query.
context.Entry(e).Statereturns anEntityState;Detachedmeans "not being tracked by the DbContext", and its "Action on SaveChanges" is none. (Change Tracking, "Entity states" table) [fetched] - No-tracking results (
AsNoTracking, orQueryTrackingBehavior.NoTrackingon the context) are plain instances; changes to them are not persisted bySaveChanges. (Tracking vs. No-Tracking, "No-tracking queries") [fetched] - Keyless entity types "are never tracked for changes in the DbContext and therefore are never inserted, updated or deleted"; their mapped object is "treated as a read-only query source". (Keyless Entity Types, "characteristics", "Mapping to database objects") [fetched]
- Attempting to
Attach/Updatea keyless instance throwsInvalidOperationExceptionwithCoreStrings.KeylessTypeTracked: "Unable to track an instance of type '{type}' because it does not have a primary key." (CoreStrings.KeylessTypeTracked) [search] - The model answers keylessness:
IReadOnlyEntityType.IsKeyless/FindPrimaryKey() == null. [memory] - Pitfall: modifying a no-tracking instance and calling
SaveChangesis silent; nothing is written and nothing is thrown.ChangeTracker.HasChanges()returns false (Change Tracking page, closing tip). [fetched]
Hibernate: @Immutable and read-only entities
- Signal shape: query on the session.
Session.isReadOnly(Object entityOrProxy): "Is the specified entity or proxy read-only?";isDefaultReadOnly()for the session default;setReadOnly(Object, boolean)flips one entity. (Session javadoc 6.4) [fetched] - Read-only can be set per session (
setDefaultReadOnly), per query (Query.setReadOnly(true), before execution) or per entity. (Hibernate 4.3 manual ch. 12, 12.1.2-12.1.3) [fetched] - Write behaviour: changes to read-only entities are ignored at flush, no exception ("Hibernate will not update simple properties or updatable single-ended associations"); "A read-only entity can be deleted." (ch. 12.1) [fetched]
@Immutable: "Changes made in memory to the state of an immutable entity are never synchronized to the database. The changes are ignored, with no exception thrown." Immutable collections, by contrast, throwHibernateExceptionon add or remove. (@Immutable javadoc 6.2) [fetched]- Bulk HQL updates of immutable entities: setting
hibernate.query.immutable_entity_update_query_handling_modechooseswarning(default) orexception. (QuerySettings 6.6) [search] - Pitfall: the default for entity changes is silent loss; the read-only state is per session, so the same row is writable in another session.
ActiveRecord: readonly?
- Signal shape: property on the record.
readonly?"returns true if the record is marked as read-only";readonly!marks it. (ActiveRecord::Core) [fetched] - Relation-level:
User.readonlymarks every loaded record;users.first.save # => ActiveRecord::ReadOnlyRecord: User is marked as readonly. (QueryMethods#readonly) [fetched] - Write behaviour: save, update or destroy raise
ActiveRecord::ReadOnlyRecord. [fetched] - The flag is set when the record is loaded and lives on the in-memory object; it is not re-derived from the database or the model at write time. [memory]
2. Document stores and embedded databases
Realm (Kotlin SDK): frozen objects
- Signal shape: property functions on the object.
BaseRealmObject.isFrozen(): "A frozen object is tied to a specific version of the data in the realm and fields retrieved from this object instance will not update"; alsoisManaged()andisValid()("the Realm is open and the underlying object has not been deleted"). (BaseRealmObjectExt.kt L41-71) [fetched] - Recovery path:
MutableRealm.findLatest(obj)returns a live, writable copy inside a write block, ornull"if the object has been deleted". (MutableRealm.kt L50-56) [fetched] - Write behaviour: writing to a frozen object throws. Early SDKs threw a generic
"Wrong transactional state ..." message; issue #297 asked for "Frozen objects
cannot be modified" plus a pointer to
findLatest. (realm-kotlin#297) [fetched] - Pitfall:
isFrozen()is stable for a given instance, butisValid()is live and can flip when another writer deletes the object.
Couchbase Lite
- Signal shape: type. "By default, a document is immutable when it is read
from the database. Use the Document.ToMutable() to create an updatable
instance."
DocumentversusMutableDocumentmakes the distinction static. (Couchbase Lite C# Documents, "Mutability") [fetched] - Write conflicts with other writers are handled at save by concurrency control (last-write-wins or fail-on-conflict) or a conflict handler. [memory]
LiteDB
- Signal shape: store-level option only. Connection string
ReadOnly("Open datafile in read-only mode"). No per-document read-only state found. (LiteDB connection string) [fetched]
SQLite
- Signal shape: store query.
sqlite3_db_readonly(D,N)"returns 1 if the database N of connection D is read-only, 0 if it is read/write, or -1 if N is not the name of a database". (sqlite3_db_readonly) [fetched] sqlite3_stmt_readonly()answers whether a prepared statement writes. [memory]
Core Data
- Signal shape: store property plus an open option.
NSPersistentStore.isReadOnly: "A Boolean value that indicates whether the persistent store is read-only" (get/set);NSReadOnlyPersistentStoreOption: "A flag that indicates whether a store is treated as read-only", default false. (isReadOnly, NSReadOnlyPersistentStoreOption) [fetched] - Granularity: store, not object. An object's store is reachable through its
objectID.persistentStore. [memory]
3. Serializers and schema evolution: unknown or foreign data
Protocol Buffers
- Unknown fields: "Proto3 messages preserve unknown fields and include them during parsing and in the serialized output." They are lost on JSON serialization or when copying field by field into a new message. (proto3 guide, Unknown Fields) [fetched]
- Signal shape for unknown fields: property/list on the message,
getUnknownFields()returning anUnknownFieldSet(Java). [memory] Anywith a foreign type: queryany.is(Foo.class)returns a boolean;unpack(Class)throwsInvalidProtocolBufferException. (Any javadoc) [fetched]- Granularity: fields inside a known message, or one embedded payload; no ownership or write-permission concept.
Apache Avro
- Writer fields absent from the reader schema are dropped: "the writer's value for that field is ignored". A union branch the reader lacks: "if none match, an error is signalled". (Avro 1.11.1 spec, Schema Resolution) [fetched]
- Signal shape: exception only for unresolvable data; nothing marks a record as carrying dropped fields.
Unity SerializeReference: missing types (closest found to CultCache)
- A host asset holds managed references whose recorded type (assembly, namespace, class) is not in the current project.
- Signal shape: query returning a bool and a list.
SerializationUtility.HasManagedReferencesWithMissingTypes(Object obj): "returns true if one or more managed references is missing its type."GetManagedReferencesWithMissingTypesreturns theManagedReferenceMissingTypeentries:assemblyName,className,namespaceName,referenceId,serializedData. (HasManagedReferencesWithMissingTypes, ManagedReferenceMissingType) [fetched] - Behaviour: the field stays null, the persisted state is retained on resave, and
if the type returns later, "the state of the Managed Reference object can be
recovered".
ClearManagedReferenceWithMissingTyperemoves one deliberately. (GetManagedReferencesWithMissingTypes) [search] - Both calls are made on demand against a host object; the docs do not say the
list is cached at load. A sibling
PrefabUtility.HasManagedReferencesWithMissingTypesexists for prefab instances. [search]
.NET DataContractSerializer: IExtensibleDataObject
- Unknown members round-trip through an opaque
ExtensionDataObjectproperty on the object. It cannot be inspected, so it is a carrier, not a signal. [memory]
4. File systems and object stores
POSIX access(2) and TOCTOU
- Signal shape: query on the path.
access(path, W_OK)fails withEROFSon a read-only file system. The check uses the real UID/GID, "rather than the effective IDs as is done when actually attempting an operation". (access(2)) [fetched] - Pitfall: checking before
open(2)"creates a security hole" because the state can change between check and use (access(2) NOTES). [fetched] - Python's
os.accessdocs repeat the warning and show the EAFP alternative: try theopenand catchPermissionError. (os.access) [fetched] - CWE-367 defines TOCTOU as checking "the state of a resource before using that resource, but the resource's state can change between the check and the use". Mitigations listed: drop the check, make check and use atomic, lock before the check, recheck after use, shrink the window. (CWE-367) [fetched]
Amazon S3 Object Lock
- Signal shape: metadata on the object version, read with HEAD/GET.
HeadObjectreturnsx-amz-object-lock-mode(GOVERNANCE | COMPLIANCE),x-amz-object-lock-retain-until-dateandx-amz-object-lock-legal-hold, "only returned if the requester has thes3:GetObjectRetentionpermission" (legal hold:s3:GetObjectLegalHold). Dedicated calls exist as well (GetObjectRetention,GetObjectLegalHold). (HeadObject) [fetched] - Write behaviour: a versioned DELETE of a locked version returns
403 Access Denied; a simple DELETE succeeds and adds a delete marker. A PUT to the same key succeeds and creates a new version; the old version stays locked. Governance mode can be bypassed withs3:BypassGovernanceRetentionplus thex-amz-bypass-governance-retention:trueheader. (Object Lock, "How deletes work", "Retention modes") [fetched] - Pitfalls: the signal depends on read permission (absent header does not mean
unlocked); legal holds can be "freely placed and removed" by anyone with
s3:PutObjectLegalHold, so the answer can change between HEAD and DELETE; under variable retention the retain-until date "moves forward as time passes" while an event hold is on. (Object Lock page, "Legal holds", "Retain-until-date behavior") [fetched]
Git LFS lockable files
- Ownership signal enforced through the file system: files marked lockable "are
made read-only in the working copy when not locked by the current user"
(
lfs.setlockablereadonly, default true). (git-lfs-config, L448-455) [fetched] - The authoritative check is server-side at push: Git LFS "verifies that pushes don't alter files locked by others". (git-lfs-lock) [fetched]
- Pitfall: the read-only bit reflects lock state as of the last checkout or lock
command;
chmoddefeats it locally, and the push check decides. [memory for the staleness; the push check is fetched]
5. Collections (the type-system version of the same question)
- .NET
ICollection<T>.IsReadOnly: a property. Read-only here "does not indicate whether individual elements of the collection can be modified"; an array cast toICollection<T>reports true while its elements stay writable. Mutators throwNotSupportedException. (ICollection.IsReadOnly) [fetched; the exception is memory] - Java
Collection: exception only. Mutators are "optional operations" that throwUnsupportedOperationException; the interface has no query for modifiability. (Collection, Java 21) [fetched]
6. Summary table
| System | Granularity | Signal shape | Write when read-only | Can the answer go stale? |
|---|---|---|---|---|
| EF Core no-tracking | entity instance | query (Entry(e).State) |
silent no-op | yes, attach/detach changes it |
| EF Core keyless | entity type | model query (IsKeyless) [memory] |
throws on attach/update | no (model is fixed after build) |
| Hibernate read-only | entity in session | query (Session.isReadOnly(e)) |
ignored at flush; delete allowed | per session; settable |
| Hibernate @Immutable | entity type | annotation | ignored, no exception; bulk HQL warns or throws | no |
| ActiveRecord | record | property (readonly?) |
raises ReadOnlyRecord |
set at load, not re-derived [memory] |
| Realm frozen | object version | property (isFrozen(), isValid()) |
throws; findLatest for a writable copy |
isValid is live |
| Couchbase Lite | document | type (Document vs MutableDocument) |
n/a, no mutators | no |
| LiteDB, SQLite, Core Data | store or database | option plus store query/property | refused by the store | rarely |
| protobuf unknown fields / Any | field / payload | property list [memory] / is() query |
preserved / unpack throws | no |
| Avro | field / union branch | exception only | field dropped / error | no |
| Unity missing types | managed reference in a host | query (bool) plus list (key, type name, data) | data preserved; recoverable when type returns | answered on demand |
| POSIX access(2) | path | query | open fails | yes, TOCTOU (documented) |
| S3 Object Lock | object version | metadata via HEAD/GET | versioned delete 403; PUT makes a new version | yes, holds and event holds change |
| Git LFS lockable | file | file-system read-only bit | push refused by server | yes, as of last checkout |
| .NET ICollection | collection | property | NotSupportedException |
no |
| Java Collection | collection | exception only | UnsupportedOperationException |
n/a |
Patterns observed, stated as facts across the sources above:
- Every system that offers an advance signal still has the write path decide: EF Core, Realm, ActiveRecord, S3, Git LFS and POSIX all refuse or ignore at the write regardless of what a check said.
- Where the read-only state can change after it was observed (POSIX permissions, S3 holds, Git LFS locks, EF Core attach/detach), the documentation either warns of TOCTOU (POSIX, Python, CWE-367) or places the authoritative check at the write (S3, Git LFS push).
- Silent-ignore write behaviour (Hibernate entities, EF Core no-tracking) is the case where the advance signal is the only signal; Hibernate added an opt-in exception mode for bulk updates.
- The closest analog to records under an unregistered schema id (Unity missing types) exposes both an on-demand bool query and an on-demand list carrying the reference id and the recorded type name, and recovers the data when the type returns.
- Store-level read-only (LiteDB, SQLite, Core Data, and CultCache's own
CacheBackingStore.IsReadOnly) is a property or query on the store, not on the record.