oschema rootobject CRUD (SmartID file-store): full-replace set can drop fields + N+1 list/find — document or harden #157
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_lib#157
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The oschema
[rootobject]CRUD generator (crates/derive/src/openrpc_from_oschema.rs, emitting<type>.{new,get,set,delete,list,list_full,exists,find}) is backed by a SmartID file-store (one<sid>.otomlfile per object) with two sharp edges that make it unsafe/slow for services whose objects have derived/computed/array fields or whose collections are high-volume / hot-listed. This isn't a single-service bug — every oschema-first migration hits the "use generated rootobject storage vs. hand-write handlers" decision and runs into it. Filing so it's tracked at the framework level rather than re-discovered per migration.Already-settled context (so this isn't re-litigated)
rootobject." So the high-level guidance — don't markrootobjectif you need custom storage — exists. This issue is only about the two specific, currently-undocumented failure modes below.ClientErrorimport path) — both fixed, different bugs.Verified mechanism
Confirmed against the generator the migrations build against (herolib
9657556), incrates/oschema/src/collection/mod.rs. The store is filesystem OTOML, not SQL — one<sid>.otomlper object.set(&self, obj: &mut T)(~L241): serializes the whole typed object to OTOML and atomically overwrites its file (fs::writetmp +fs::rename). Full-object replace, no merge. → Lossy only when the typed objectTisn't the complete source of truth: partial sets, or fields the service maintains outsideT(derived/computed/array sub-collections) get dropped with no error. If the schema type holds every field,setis safe.list(&self)(~L287):fs::read_dir→ returns SmartIds only. Materializing full objects isget()per id → N+1 file reads. Fine for dozens/hundreds; slow for high-volume / frequently-listed collections.find/ indexing:finditerates;has_index()returnsfalse— no query engine, joins, or indexes.Feasibility today (unchanged by the shipped codegen fixes)
Evidence
hero_collab/docs/superpowers/collab-oschema-review-2026-06-15.md(4-reviewer architecture review). Generated_setwould have dropped message-derived fields; sid-array_list/_findreproduced the hero_planner N+1.Ask
Either:
setmerge semantics (or explicit field-scoping so derived/array fields are clearly owned by custom handlers) and a batched/indexedlist/findinstead of N+1get-per-id; orPointers
crates/oschema/src/collection/mod.rs(set~L241,list~L287).crates/derive/src/openrpc_from_oschema.rs.hero_collab/docs/superpowers/collab-oschema-review-2026-06-15.md.Correction / cross-reference (the framework already establishes the core principle).
Closed #63 ("fix / improve code generation") already states the design intent: "the root objects are to support single CRUD implementation. Any schema that wants to use its own backend should not be marked
rootobject." So the high-level guidance — don't markrootobjectif you need custom storage — is already settled, and I shouldn't have framed this as wholly undocumented. (For the record, the other closed codegen issues — #153 description off-by-one, #101 OSIS SDK ClientError path — are unrelated bugs, already fixed.)What remains genuinely open (narrowing this issue to it): the specific failure modes a developer hits if they DO mark a rich object
rootobject, which #63 doesn't call out:_setreplaces the stored record wholesale → server-derived / computed / array / extra fields are silently dropped (no error);_list/_findare N+1 (per-row fetch).Ask for this issue: either (a) harden the generated CRUD (
_setmerge semantics, single-query_list), or (b) just document these two specific traps next to the #63 principle so migrators recognize them by name. If the maintainers consider #63 sufficient and don't want the extra hardening/docs, this can be closed as covered.Not re-verified against the very latest generator beyond the hero_collab review (2026-06-15, current stack) + the hero_planner N+1 — flag if #63's follow-ups already changed
_set/_listbehavior, in which case close this.Verified against the current generator (herolib
9657556, the rev the migrations build against) — confirming the mechanism rather than the review-doc summary.The generated rootobject persistence is a SmartID file-store, not SQL: one
<sid>.otomlfile per object. Source:crates/oschema/src/collection/mod.rs.set(&self, obj: &mut T)(~L241): serializes the whole typed object to OTOML and atomically overwrites its file (fs::writetmp +fs::rename). It is a full-object replace, no merge. So this is lossy only when the typed objectTisn't the complete source of truth — i.e. partial sets, or fields the service maintains outsideT(derived/computed/array sub-collections). If the schema type holds every field,setis safe. (This refines the earlier "silent data loss" wording.)list(&self)(~L287):fs::read_dir→ returns SmartIds only. Materializing full objects isget()per id → N+1 file reads. Inherent to the file-store; fine for dozens/hundreds, slow for high-volume/hot-listed collections.find/ no index: iterates;has_index()returnsfalse— no query engine, joins, or indexes.Feasibility (unchanged by the shipped codegen fixes #63/#153/#101):
What would move the line (this issue's ask):
setmerge semantics (or explicit field-scoping) + a batched/indexedlistinstead of N+1get-per-id. Otherwise, document these two traps by name next to the #63 guidance.oschema rootobject CRUD codegen: generatedto oschema rootobject CRUD (SmartID file-store): full-replace_setcan silently drop fields +_list/_findare N+1 (fix or document)setcan drop fields + N+1list/find— document or hardenDownstream consumer instance: hero_planner #30 — planner's workspace load is N+1 (
listEntitiesreconstructs via_find+_get) precisely because the generated rootobject*_list_fullstubs return empty, i.e. the consumer symptom of this generator behavior. Fixinglist/findhere (batched/indexed, or real*_list_full) resolves it for planner (and any other rootobject consumer).Note from the CM50 OSIS convergence work: the N+1 half of this is apparently tolerated in the canonical pattern — the worked reference
hero_bizbackend/.../business_impl.rsimplementscompany_list_fullaslist()thenget()per item (N+1) on the OSISDBTypedstore, and that is the blessed example in home#309. So the actionable/contentious core here is really the_setwholesale-replace dropping derived/array fields, not the N+1 (which the team accepts for now). Suggest reframing this issue toward the_setdata-loss semantics; the N+1 is "known/accepted" unless/until a batched or indexedlist_fullis added.