Files
ng-eventually/.project/concepts/app-contract/knowledge_what-the-orm-writes-and-deletes-upstream.md
T
Sylvain Duchesne 7bcb52a5fd fix: retire la refutation de where/orderBy/pageSize, lue dans le mauvais arbre
Deux feuilles affirmaient que la richesse de la config ORM est perdue au
niveau 1 - "no caller can reach any of it. The richness is real and
unreachable". La preuve invoquee etait le `json!({})` de
`Verifier::start_orm` a 213338f6, et elle ne porte pas cet enonce.

Ce qui est verifie a 213338f6 : `OrmConfig` declare where_, order_by,
select, page_size et max_active_pages ; `from_json` les analyse tous les
cinq et refuse un pageSize sans orderBy ; schema_shape_to_sparql et le
pagineur les consomment. La machinerie est implementee. Il manque un
seul saut, a l'ENTREE : start_orm ne prend aucun argument de config, et
son TODO voisin nomme le maillon absent, "use json in libwasm context".

Or ce maillon existe dans le build : @ng-org/web@0.1.2-alpha.14 publie
orm_start_graph avec SIX parametres, config en position 4, quand l'arbre
cite en declare CINQ, et @ng-org/orm@0.1.2-alpha.21 l'appelle avec six
en passant tout son objet d'options. Lus dans les artefacts publies.

Donc la config quitte le niveau 3 et est acceptee au niveau 2 aux
versions installees. Ce que le moteur derriere ce binding en fait n'est
etabli par rien d'atteignable, et c'est dit ainsi.
2026-09-24 15:05:22 +02:00

65 lines
10 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-24
---
# What the ORM writes and deletes upstream
Read at the source in `nextgraph-rs` (`213338f697df15afc086c042f4bab1bffd01aa04`), which declares `@ng-org/orm@0.1.2-alpha.18`, `@ng-org/web@0.1.2-alpha.13`, `@ng-org/shex-orm@0.1.2-alpha.8` and `@ng-org/alien-deepsignals@0.1.2-alpha.11`. `packages/polyfill` installs `@ng-org/orm@0.1.2-alpha.21`, `@ng-org/web@0.1.2-alpha.14`, `@ng-org/shex-orm@0.1.2-alpha.9` and `@ng-org/alien-deepsignals@0.1.2-alpha.13` - the published packages are AHEAD of the source this repo cites, so where the two disagree both are named below. That gap is checked rather than remembered: the `ANCHOR` in `packages/polyfill/test/provenance.test.ts` holds it once and goes red from either end. 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 at any version: `sdk/js/orm/src/core.ts` at the cited commit exports exactly `initNg`, `ngSession`, `OrmSubscription`, `DiscreteOrmSubscription`, `insertObject`, `getObjects`, plus `getRaw`/`watch`/`effect` re-exported from `@ng-org/alien-deepsignals`. The installed `@ng-org/orm@0.1.2-alpha.21` renames `OrmSubscription` to `RdfOrmSubscription` and adds one export the source does not carry, `removeObject(graphNuri, subjectIri): Promise<void>` - see the note at the end of this section. 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.
**`removeObject` exists in the package and not in the source.** Its own documentation declares a wider effect than the set mutation: `DELETE WHERE { GRAPH <graphNuri> { <subjectIri> ?p ?o } }`, every predicate of the subject rather than the shape's own, while claiming in the same comment to have "the same effect" as a `delete()` on a subscription's object. The two cannot both hold, and nothing in the cited source settles it. It stays unpublished here for the reason `contract_polyfill-surface` records: a symbol whose only evidence is an npm build is not something to build a guarantee on.
## 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` (`RdfOrmSubscription` in alpha.21), 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`** (`RdfOrmSubscription` in alpha.21) 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`**, `RdfOrmSubscription` in alpha.21 — 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`. In alpha.21 the second argument is no longer a bare `Scope` but an `RdfOrmConfig` (`Scope & { where?, orderBy?, pageSize?, maxActivePages? }`), `readyPromise` resolves to the subscription data rather than to nothing, and the instance also carries `mode`, `isReady`, `addChangeListener`, `removeChangeListener`, `nextPage` and `previousPage`. How far that config travels is answered in [[knowledge_what-a-read-scope-is-upstream]]: the shipped binding takes it at index 4 and the shipped ORM sends it there, while what the engine behind that binding does with it is settled by nothing reachable. The `json!({})` in `213338f6`'s `Verifier::start_orm` does NOT settle it, and reading it as *"level 1 discards the richness"* - the claim this leaf used to carry - is inference from an absent implementation. 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 STARTS against the deployment, and the delete reaches the document. `packages/polyfill/e2e/orm-live-read.ts` is the measurement - `5 passed, 0 failed` on 2026-09-21, the read ready in 13ms, the seeded subject's triples gone after `.delete()`.
The read was blocked until 2026-09-21, and the recorded reason was wrong twice over. It was not that the broker needed to catch up: the DEPLOYMENT was ahead and the pinned client packages were behind, `orm_start_graph` having gained a `config` parameter at index 4, before the callback, which the five-argument client of the day filled with its callback. Updating the packages answered it. The bug leaf that carried the wrong mechanism was deleted with the question it described.