test: Make the DST more robust (#116)

This commit is contained in:
Marcelo Mendonça Soares 2026-09-14 10:26:56 -03:00 • committed by GitHub
commit 574f8d067a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -53,13 +53,27 @@ soft-deleted instead of removed:
FK is nullable, and those child FK fields receive ordinary CRDT field updates.
- `ON DELETE SET DEFAULT`: visible child FK columns are updated to the column
default when the default is legal, and those child FK fields receive ordinary
CRDT field updates.
CRDT field updates. A nullable null default is legal; a non-null default must
remain visible in the same scope after the delete. A default in the same
delete batch is unavailable. If a visible child needs this repair, an invalid
default rejects the transaction, including any earlier repairs or tombstones
in that transaction. An unused invalid default does not itself block a delete.
- `ON DELETE CASCADE`: visible cascade descendants receive synced
user-delete tombstones. Local cascade descendants are not hidden only as
`foreignKeyCascade` projection rows.
These local side effects are the facts that other replicas merge.
Nullable fields must preserve explicit nulls when models are serialized and read
back. For a nullable FK with a non-null database default, use Serverpod's
`defaultPersist` declaration: the combined `default` also supplies a constructor
fallback that replaces null on model reads. Materialized inserts retain nulls
chosen by projection instead of applying the database default again. Local
upserts apply a fixed UUID FK default to null input fields. This path does not
distinguish an omitted field from an explicitly supplied null; preserving a
projection-selected null does not imply that an upsert can author null on such
a column.
## Merge-Time Action Semantics
For each child FK whose attempted value points to a hidden or missing parent:
@ -84,6 +98,57 @@ would cascade-delete `B`, but `B` has a visible `RESTRICT` child `C`, then `A`,
`B`, and `C` remain visible. The result must not depend on whether cascade or
restrict edges are evaluated first internally.
Hidden children do not block parent deletions. They still participate in FK
projection: when no legal repair exists and the authored parent remains
physically present in the same scope, the FK candidate retains the authored
reference, even if that parent is hidden. This recomputes from authored values;
it does not preserve an earlier projected fallback. Unique projection can then
release that candidate, as described in the combined projection model below.
Default targets do not have to be permanent. Projection starts from all winning
authored delete tombstones in the affected closure and computes their candidate
cascade closures. It evaluates repairs against that candidate visibility,
withdraws blocked deletion roots together, and repeats until no root is
withdrawn. A withdrawn root is not reintroduced during that pass. This
conservative policy terminates because the set of accepted roots only shrinks;
it need not accept the largest possible set of deletions. Every new pass starts
from the authored tombstones again, not the previous projection's accepted deletions.
For a child authored as `C -> A` with default `D`, merging an independently
authored deletion of `A` projects `C -> D` while `D` survives. If a deletion of
`D` is then merged and has no other blockers, `A` becomes visible again and
`C -> A` is restored. Merging a restoration of `D` can make `A`'s deletion take
effect again. Throughout this example the winning authored child reference
remains `C -> A`; the derived repairs do not advance its HLC. A local deletion
of `A` with this visible child instead authors `C -> D`, so it is a different
set of facts and does not have this restoration behavior.
A default is a schema dependency even while no child currently references it.
Projection loads defaults as evaluation context, but does not treat that alone
as a change to every consumer. It follows authored FK references (including new
overlays) to determine which defaults are affected, excluding relationships that
exist only because an earlier projection rewrote a child onto its fallback. A
live standalone default with no authored delete cannot change visibility merely
because a child's FK is updated. For defaults referenced by primary key, that
common case skips the dependency walk.
When a default is changed directly, or affected through a possible deletion,
cascade, or missing parent, the loader queries sparse dependency seeds in the
current scope: authored tombstones and hidden rows in the original-parent and
cascade-ancestor tables, hidden children, and children with FK overrides. The
ordinary reference walk then loads their children, parents, and unique claimants.
Blocked deletions are included even when the original parent is still visible
and the child has no override. Dependency loading repeats until no new default
edge needs expansion. Deletion arbitration then runs on the loaded closure,
starting from authored tombstones.
This keeps ordinary merges on the existing row-closure path; there is no
component-wide domain-row load. Default-specific invalidation can still visit
many rows when many deletes, hidden rows, or repair claims depend on that default.
The sparse metadata lookup itself may scan rows in the relevant scoped metadata
tables, but it runs only for affected defaults. Full rebuilds still load the
complete requested components.
## Combined Projection Boundary
The attempted-value schema, hidden-row lifecycle, exact FK-before-unique
@ -183,7 +248,7 @@ The combined FK/unique, hidden-row restoration, batching, and unrelated-update
cases are centralized under the
[projection model verification gates](projection-model.md#verification-gates).
## What These Tests Prove
## What These Tests Check
- Integrity: no surviving visible FK violation.
- Convergence: same merged facts produce the same projection on every replica.