61 lines
7.4 KiB
Markdown
61 lines
7.4 KiB
Markdown
---
|
|
type: knowledge
|
|
summary: What the target offers for writing and removing — deletion is a SET MUTATION wired end to end, it removes the shape's predicates rather than the subject, and the ORM's writes never pass through `sparql_update`
|
|
last_checked: 2026-09-16
|
|
---
|
|
|
|
# What the ORM writes and deletes upstream
|
|
|
|
Read at the source in `nextgraph-rs` (`213338f697df15afc086c042f4bab1bffd01aa04`), with `@ng-org/orm@0.1.2-alpha.19`, `@ng-org/web@0.1.2-alpha.13` and `@ng-org/alien-deepsignals@0.1.2-alpha.11` installed in `packages/polyfill`. Every claim points at a symbol rather than a line, so `git grep` settles any doubt after the code moves. Companion to [[knowledge_what-a-read-scope-is-upstream]], which answers the reading half.
|
|
|
|
## Removing an object is a set mutation, and LEVEL 3 answers
|
|
|
|
`sdk/js/orm/README.md` teaches it as one line — `dogs.delete(aDog)` — on the `DeepSignalSet` a subscription hands back. There is no `deleteObject` symbol and no deletion file: `sdk/js/orm/src/core.ts` exports exactly `initNg`, `ngSession`, `OrmSubscription`, `DiscreteOrmSubscription`, `insertObject`, `getObjects`, plus `getRaw`/`watch`/`effect` re-exported from `@ng-org/alien-deepsignals`. That absence is **not** the finding, and per [[no-divergence-from-nextgraph]] it would prove nothing on its own — the finding is that the set mutation is fully implemented, so the cascade stops at level 3 and there is nothing to invent.
|
|
|
|
## The path, hop by hop
|
|
|
|
1. **The set proxy.** `deepSignal`'s set handler (`sdk/js/alien-deepsignals/src/deepSignal.ts`, the `delete` branch) emits, for an OBJECT member, `{ path: [...containerPath, syntheticId], op: "remove" }` — a `DeepPatch` with no `type`. For a LITERAL inside a multi-valued property it emits `{ path: containerPath, op: "remove", type: "set", value }` instead.
|
|
2. **The synthetic id is `"<graph>|<subject>"`**, minted by `OrmSubscription.signalObjectPropGenerator`, which also generates a subject IRI (`<first 53 chars of the graph NURI>:q:<33 random bytes base64url>`) when `@id` is `""`.
|
|
3. **`deepPatchesToWasm`** (`sdk/js/orm/src/connector/utils.ts`) joins the path into the string `/<graph>|<subject>`.
|
|
4. **`OrmSubscription.onSignalObjectUpdate`** batches in a microtask (or into `pendingPatches` inside a transaction) and calls `ng.graph_orm_update(subscriptionId, patches, session_id)`.
|
|
5. **The binding.** `graph_orm_update` (`sdk/js/lib-wasm/src/lib.rs`) deserialises `OrmPatches` — `OrmPatch { op, valType, path, value }` with `OrmPatchOp::{add, remove}` and `OrmPatchType::set` (`engine/net/src/orm.rs`) — into `AppRequest::new_orm_update`.
|
|
6. **The engine.** `Verifier::orm_frontend_update` → `create_sparql_update_query_for_patches` (`engine/verifier/src/orm/graph/handle_frontend_update.rs`): a `remove` whose path resolves to a ROOT object (one segment, no predicate) calls `SparqlBuilder::remove_object` and marks the tracked object `TrackedOrmObjectValidity::Invalid`. The queries are then run through `process_sparql_update`, anchored on the graph taken from the first patch's path.
|
|
|
|
**The trap that makes a delete a no-op.** The patch is emitted only when `target.delete(rawValue)` returned true **and** a synthetic id is known for that object. So the object passed must be the one the set is holding — reached by iterating it, by `.first()` or by `.getBy(graph, subject)`. `getObjects` returns a `deepClone`, so deleting an item of ITS result removes nothing and reports nothing.
|
|
|
|
## `remove_object` deletes the SHAPE's predicates, not the subject and not the document
|
|
|
|
`SparqlBuilder::remove_object` loops the shape's own `predicates` and emits one `DELETE { GRAPH <g> { <s> <p> ?o } } WHERE { … }` per predicate. Triples on the same subject under a predicate the shape does not declare **survive**, and so does the document: nothing on this path deletes a document, and the ORM offers no call that does.
|
|
|
|
Removing a child from a parent's multi-valued object property is a different arm — `remove_link`, a `DELETE DATA` of the one triple that ties parent to child. The README states the consequence plainly: only that tie goes, the child's own quads stay.
|
|
|
|
## `getObjects` and `insertObject` — the exact surface
|
|
|
|
```ts
|
|
getObjects<T extends BaseType>(shapeType: ShapeType<T>, scope: Scope | string): Promise<DeepSignalSet<T>>
|
|
insertObject<T extends BaseType>(shapeType: ShapeType<T>, object: T): Promise<void>
|
|
```
|
|
|
|
Both return types are **inferred**, not annotated, and both functions are thin wrappers over `OrmSubscription`.
|
|
|
|
- **`getObjects`** (`sdk/js/orm/src/connector/getObjects.ts`) — `getOrCreate(shapeType, normalizeScope(scope))`, awaits `readyPromise`, schedules `close()` one second later, returns `deepClone(signalObject)`. The result is **detached**: a plain `Set` of plain objects, no two-way binding, no reactivity. `docs/api-contract.md` § 6 records this accurately — checked against the source, not re-derived.
|
|
- **`insertObject`** (`sdk/js/orm/src/connector/insertObject.ts`) — opens a subscription scoped to `{ graphs: [] }` (deliberately subscribing to no document), awaits ready, `signalObject.add(object)`, `close()`. The object **must** carry `@graph`: a root object without one makes `signalObjectPropGenerator` throw *"When adding new root orm objects, you must specify the @graph"*. `@id: ""` means "generate one".
|
|
|
|
## The three remaining symbols
|
|
|
|
**`OrmSubscription`** — a class, and the machinery every other read and write goes through. Private constructor; `static getOrCreate(shapeType, scope)` pooled on `"<shape>|<graphs>|<subjects>"` with refcounting and a 500 ms close delay. Instance surface: `signalObject: DeepSignalSet<T>`, `readyPromise`, `close()`, `beginTransaction()`, `commitTransaction()`, `inTransaction`, `shapeType`, `scope`. It hands a caller nothing the target will not hand them — a shape, a scope and a set — and `useShape`, `getObjects` and `insertObject` are all built on it.
|
|
|
|
**`DiscreteOrmSubscription`** — the same class shape for JSON-CRDT documents. `static getOrCreate(documentId: string)`; `signalObject` is `DeepSignal<DiscreteArray | DiscreteObject> | undefined` (a getter, undefined before the initial payload); it reaches the engine through `orm_start_discrete` and `discrete_orm_update`. Its unit of subscription is **one document id**, never a `Scope`.
|
|
|
|
**`ngSession`** — not a function and not a class: a module-level `Promise<{ ng: NG; session: Session }>` (`sdk/js/orm/src/connector/initNg.ts`), resolved exactly once by `initNg`. Every subscription awaits it before touching the engine. Its value is the SDK handle plus the session object.
|
|
|
|
## A write never passes through `sparql_update`
|
|
|
|
`graph_orm_update` is its own SDK member, distinct from `sparql_update`. So a guard implemented by overriding `ng.sparql_update` — which is what `packages/polyfill/src/surface/ng-proxy.ts` does — **does not see an ORM write at all**, neither an add nor a delete. The read-filtered view (`packages/polyfill/src/emulated-verifier/read-filter.ts`) is symmetrical by design: it forwards `add`, `delete` and `clear` to the underlying set untouched, because those yield no items to leak.
|
|
|
|
That is a fact about where a guard can sit, not a proposal for one.
|
|
|
|
## Live behaviour against the deployed broker
|
|
|
|
Verified by measurement, not by reading: a live ORM read still does not start against the deployment, and the reason is **not** the one previously recorded. See [[bug_useshape-live-read-needs-a-newer-broker]], and `packages/polyfill/e2e/orm-live-read.ts`, which is the measurement.
|