Artifact f2a085d1fdc79ab8b3c9e3b4019b4448e9f294a8ff79915d431bc7947b6a3950:
- File
internal/content/deltify_test.go
— part of check-in
[2368cad91a]
at
2026-07-19 20:55:48
on branch trunk
— Deltify predecessors on the commit path (#87)
* Deltify predecessors on the commit path
Every locally-created artifact was stored whole. The delta table was in the schema and stayed empty, so a repository grew with the number of commits rather than the amount of change. blob.StoreDelta existed and worked but had no caller on the commit path; the only delta-store in use was blob.StoreDeltaRaw, from the sync receive path, for deltas that arrive pre-encoded. The write path never made a deltify decision at all.
Port canonical Fossil's content_deltify (2.28 src/content.c:852-947) as content.Deltify. Fossil deltifies backwards: a new artifact is always stored whole and its PREDECESSOR is rewritten as a delta against it, so the tip of every chain is full content and reading the newest version of a file stays a single blob load. Chain depth becomes an artifact's age in revisions rather than something that compounds, because an artifact already stored as a delta is never re-deltified.
blob.StoreDelta turned out to be the wrong primitive to wire up: it inserts a new row as a delta, whereas the policy needs to rewrite an existing row. It is left alone. content.Deltify lives in the content package because it needs Expand, which blob cannot import.
The policy is stated once, in content.Deltify, with each rule's canonical line cited: the 50-byte floors, the 75% ratio, the never-redeltify rule that bounds depth, the private-into-public refusal, and the loop break that undeltas the source and then still declines the pairing. Two call sites pass pairs, mirroring canonical's: insertMlinkRow for file content (manifest.c:1562) and Checkin for the parent manifest (checkin.c:3234). insertMlinkRow is shared by the fresh-commit and crosslink paths, so file deltification cannot drift between them.
On a 12-revision, 4-file history: 60 artifacts, 44 delta-encoded (73%), p50 chain depth 4, max 11, and 23,994 content bytes measured as sum(length(content)) -- down from 58,973 with everything whole. A repository produced this way passes fossil 2.28 test-integrity (60/60 blobs, 0 errors) and fossil rebuild, which re-derives every table by walking the chains.
* Bound the deltify ancestor walk, require a transaction, harden tests
Review follow-ups on the deltification policy.
Bound the ancestor walk. It was a bare loop with no cycle guard: a sync peer can shape a cyclic delta graph, because blob.StoreDeltaRaw records delta(rid,srcid) links from the wire without a cycle check, and an unbounded walk would spin inside the write transaction. Guard with the seen-set idiom content.walkDeltaChain already uses, plus a step cap. In the current ordering IsAvailable runs first on both rids and follows the same links under its own depth bound, so Deltify declines before the walk begins -- but that protection is incidental to a different question, so the bound belongs to the loop that needs it. Both behaviours are now pinned: the walk reports a cycle, and Deltify declines an ungrounded source.
Move both size floors ahead of the walk. Canonical returns on a sub-50-byte target (content.c:877-885) before entering the candidate loop, so it never reaches the content_undelta on :902. Running the walk first meant a small target whose successor chained back to it would inflate the source to full content and then decline anyway -- a storage regression and exactly the divergence from canonical's layout the surrounding comment claims to avoid.
Take *db.Tx instead of db.Querier. The rewrite is two statements, and a blob holding delta bytes with no delta row expands as though it were full content: silent corruption rather than a detectable error. Canonical wraps the pair in db_begin_transaction (content.c:938-941); the signature now makes that non-negotiable at compile time. Both call sites already passed a transaction.
Three tests were named for behaviours they did not pin, and each is now verified to fail when the behaviour is reverted:
- The 75% ratio test had no assertion at all; deleting the rule left it passing. It now asserts the pair is declined and stays whole. - The loop-break test could not distinguish declining from proceeding, so the undelta-then-proceed behaviour corrected in the previous commit was untested. It now asserts the target is left whole. - The manifest test checked only "at least one", so a regression from 11-of-12 to 3-of-12 passed. It now checks each index and names the manifest that stopped being encoded.
Also cover the source-side 50-byte floor, which the tiny-artifact test could not reach because it made both artifacts tiny and returned at the target check first, and assert that Undelta restores blob.size while Deltify leaves it alone (content.c:761 vs :935).
Gate the fossil-binary test on REQUIRE_FOSSIL_BIN, set in CI. A skip is invisible without -v, so a run with no fossil binary was byte-identical to a pass -- for the one criterion our own tests cannot substitute for.
Document that blob.StoreDelta is not the commit path's primitive. It inserts a new row as a delta; deltifying rewrites an existing one. It has no non-test callers and the package doc advertised it as the delta-storing entry point.
* Document what the deltify loop-safety argument rests on
Two comments, no behaviour change.
The IsAvailable(srcRid) guard is ours, not canonical's: content.c guards only content_is_available(rid) and then calls content_get(srcid) unguarded. It is also load-bearing beyond phantoms, because it screens out the cyclic delta graphs that would otherwise reach the ancestor walk. Say both, at the guard.
And say at the walk why its seen-set never fires in practice, so that someone reordering the prologue meets the reason rather than concluding the bound is dead code. The note lived only in the test file, which is not where that reader is looking. (user: [email protected], size: 11067) [annotate] [blame] [check-ins using]
package content import ( "bytes" "fmt" "strings" "testing" "github.com/danmestas/libfossil/db" "github.com/danmestas/libfossil/internal/blob" libfossil "github.com/danmestas/libfossil/internal/fsltype" ) // inTx runs fn against a *db.Tx, which Deltify and Undelta require. All // assertions happen inside the transaction, so nothing here depends on // commit behaviour. func inTx(t *testing.T, fn func(tx *db.Tx)) { t.Helper() d := setupTestDB(t) if err := d.WithTx(func(tx *db.Tx) error { fn(tx) return nil }); err != nil { t.Fatalf("WithTx: %v", err) } } // similarPair returns two bodies that share almost all of their text, so a // delta between them is far below the 75% policy threshold. func similarPair() ([]byte, []byte) { var a, b bytes.Buffer for i := 0; i < 200; i++ { fmt.Fprintf(&a, "line %04d: the quick brown fox jumps over the lazy dog\n", i) if i == 100 { fmt.Fprintf(&b, "line %04d: CHANGED\n", i) } else { fmt.Fprintf(&b, "line %04d: the quick brown fox jumps over the lazy dog\n", i) } } return a.Bytes(), b.Bytes() } func isDelta(t *testing.T, q db.Querier, rid libfossil.FslID) bool { t.Helper() src, err := deltaSource(q, rid) if err != nil { t.Fatalf("deltaSource(%d): %v", rid, err) } return src > 0 } func TestDeltifyEncodesSimilarPredecessor(t *testing.T) { inTx(t, func(tx *db.Tx) { v1, v2 := similarPair() oldRid, _, err := blob.Store(tx, v1) if err != nil { t.Fatalf("store v1: %v", err) } newRid, _, err := blob.Store(tx, v2) if err != nil { t.Fatalf("store v2: %v", err) } saved, err := Deltify(tx, oldRid, newRid) if err != nil { t.Fatalf("Deltify: %v", err) } if saved <= 0 { t.Fatalf("expected a positive saving, got %d", saved) } if !isDelta(t, tx, oldRid) { t.Error("predecessor was not delta-encoded") } if isDelta(t, tx, newRid) { t.Error("the new artifact must stay whole: deltification runs backwards") } got, err := Expand(tx, oldRid) if err != nil { t.Fatalf("Expand(old): %v", err) } if !bytes.Equal(got, v1) { t.Error("delta-encoded artifact did not expand to its original content") } }) } func TestDeltifyLeavesAlreadyDeltaAlone(t *testing.T) { inTx(t, func(tx *db.Tx) { v1, v2 := similarPair() v3 := append(bytes.Clone(v2), []byte("trailing addition\n")...) r1, _, _ := blob.Store(tx, v1) r2, _, _ := blob.Store(tx, v2) r3, _, _ := blob.Store(tx, v3) if _, err := Deltify(tx, r1, r2); err != nil { t.Fatalf("Deltify(r1,r2): %v", err) } // r1 is now a delta of r2. Re-offering it must be declined: this is // the rule that keeps chain depth linear rather than compounding. saved, err := Deltify(tx, r1, r3) if err != nil { t.Fatalf("Deltify(r1,r3): %v", err) } if saved != 0 { t.Errorf("expected an already-delta artifact to be declined, saved=%d", saved) } src, _ := deltaSource(tx, r1) if src != r2 { t.Errorf("delta source moved from %d to %d", r2, src) } }) } func TestDeltifyDeclinesTinyTarget(t *testing.T) { inTx(t, func(tx *db.Tx) { // Target under 50 bytes, source comfortably over it, so only the // target-side floor (content.c:881) can decline this pair. tiny, _, _ := blob.Store(tx, []byte("tiny target content")) big, _, _ := blob.Store(tx, []byte(strings.Repeat("tiny target content and more\n", 40))) saved, err := Deltify(tx, tiny, big) if err != nil { t.Fatalf("Deltify: %v", err) } if saved != 0 { t.Errorf("a target under %d bytes must be left whole, saved=%d", deltifyMinBytes, saved) } if isDelta(t, tx, tiny) { t.Error("tiny target was delta-encoded") } }) } // TestDeltifyDeclinesTinySource covers the source-side floor at // content.c:911, which the tiny-target case above cannot reach: that one // returns at the target check first. func TestDeltifyDeclinesTinySource(t *testing.T) { inTx(t, func(tx *db.Tx) { // The target clears 50 bytes comfortably, so the source-side floor // is the only rule that can decline this pair. big, _, _ := blob.Store(tx, []byte(strings.Repeat("shared prefix line for the delta test\n", 40))) tiny, _, _ := blob.Store(tx, []byte("short\n")) saved, err := Deltify(tx, big, tiny) if err != nil { t.Fatalf("Deltify: %v", err) } if saved != 0 { t.Errorf("a source under %d bytes must not be used, saved=%d", deltifyMinBytes, saved) } if isDelta(t, tx, big) { t.Error("artifact was delta-encoded against a sub-50-byte source") } }) } // TestDeltifyDeclinesDissimilarContent pins the 75% ratio rule // (content.c:917): a delta that does not save at least a quarter of the // target is not worth the indirection on every read. The two bodies below // share no runs long enough for the delta encoder to exploit. func TestDeltifyDeclinesDissimilarContent(t *testing.T) { inTx(t, func(tx *db.Tx) { var a, b bytes.Buffer for i := 0; i < 200; i++ { fmt.Fprintf(&a, "alpha %04d aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", i) fmt.Fprintf(&b, "%04d zzzz beta gamma delta epsilon zeta\n", i*7919) } r1, _, _ := blob.Store(tx, a.Bytes()) r2, _, _ := blob.Store(tx, b.Bytes()) saved, err := Deltify(tx, r1, r2) if err != nil { t.Fatalf("Deltify: %v", err) } if saved != 0 { t.Errorf("dissimilar pair must be declined, saved=%d", saved) } if isDelta(t, tx, r1) { t.Error("dissimilar pair was delta-encoded; the 75% ratio rule did not fire") } }) } func TestDeltifyRefusesPrivateIntoPublic(t *testing.T) { inTx(t, func(tx *db.Tx) { v1, v2 := similarPair() pub, _, _ := blob.Store(tx, v1) priv, _, _ := blob.Store(tx, v2) if err := MakePrivate(tx, int64(priv)); err != nil { t.Fatalf("MakePrivate: %v", err) } saved, err := Deltify(tx, pub, priv) if err != nil { t.Fatalf("Deltify: %v", err) } if saved != 0 { t.Errorf("expected private->public to be declined, saved=%d", saved) } if isDelta(t, tx, pub) { t.Error("a public artifact must never be stored as a delta against a private one") } }) } // TestDeltifyBreaksLoopAndDeclines pins the exact canonical behaviour at // content.c:900-908: when rid is already an ancestor of srcRid, the source is // undeltaed to break the dependency AND the pairing is declined for now. // Undeltaing but then proceeding would also be loop-free, so the decline is // the half that only an explicit assertion catches. func TestDeltifyBreaksLoopAndDeclines(t *testing.T) { inTx(t, func(tx *db.Tx) { v1, v2 := similarPair() r1, _, _ := blob.Store(tx, v1) r2, _, _ := blob.Store(tx, v2) // Make r2 a delta of r1, then ask for the reverse. if _, err := Deltify(tx, r2, r1); err != nil { t.Fatalf("Deltify(r2,r1): %v", err) } if !isDelta(t, tx, r2) { t.Fatal("setup failed: r2 is not a delta") } saved, err := Deltify(tx, r1, r2) if err != nil { t.Fatalf("Deltify(r1,r2): %v", err) } if isDelta(t, tx, r2) { t.Error("source should have been undeltaed to break the loop") } if saved != 0 { t.Errorf("canonical declines the pairing after breaking the loop, saved=%d", saved) } if isDelta(t, tx, r1) { t.Error("target must be left whole: canonical declines rather than proceeding") } for _, rid := range []libfossil.FslID{r1, r2} { if _, err := Expand(tx, rid); err != nil { t.Errorf("Expand(%d) after loop break: %v", rid, err) } } }) } // TestDeltifyBreaksLoopTerminatesOnCycle pins the bound on the ancestor // walk. blob.StoreDeltaRaw records delta(rid,srcid) links straight from the // wire with no cycle check, so a sync peer can shape A->B->A; an unbounded // walk would spin forever inside the write transaction. // // This exercises deltifyBreaksLoop directly rather than going through // Deltify, because Deltify cannot currently reach the walk with a cyclic // source: IsAvailable runs first on both rids and follows the same delta // links under its own maxAvailabilityChainDepth bound, so it returns false // and Deltify declines before the walk begins. That protection is // incidental -- IsAvailable is answering a different question // (groundedness), and reordering or relaxing it would expose the walk. The // bound belongs to the loop that has it, so the test does too. func TestDeltifyBreaksLoopTerminatesOnCycle(t *testing.T) { inTx(t, func(tx *db.Tx) { v1, v2 := similarPair() target, _, _ := blob.Store(tx, append(bytes.Clone(v1), []byte("target\n")...)) a, _, _ := blob.Store(tx, v1) b, _, _ := blob.Store(tx, v2) // Forge the cycle directly, the way a hostile peer's deltas land. for _, link := range [][2]libfossil.FslID{{a, b}, {b, a}} { if _, err := tx.Exec("REPLACE INTO delta(rid, srcid) VALUES(?, ?)", link[0], link[1]); err != nil { t.Fatalf("forge delta link: %v", err) } } _, err := deltifyBreaksLoop(tx, target, a) if err == nil { t.Fatal("expected a cycle to be reported, got success") } if !strings.Contains(err.Error(), "cycle") { t.Errorf("expected a cycle error, got: %v", err) } }) } // TestDeltifyDeclinesUngroundedSource records the belt-and-braces behaviour // the test above documents: a cyclic delta graph reaching Deltify is // declined, not an error and not a hang. func TestDeltifyDeclinesUngroundedSource(t *testing.T) { inTx(t, func(tx *db.Tx) { v1, v2 := similarPair() target, _, _ := blob.Store(tx, append(bytes.Clone(v1), []byte("target\n")...)) a, _, _ := blob.Store(tx, v1) b, _, _ := blob.Store(tx, v2) for _, link := range [][2]libfossil.FslID{{a, b}, {b, a}} { if _, err := tx.Exec("REPLACE INTO delta(rid, srcid) VALUES(?, ?)", link[0], link[1]); err != nil { t.Fatalf("forge delta link: %v", err) } } saved, err := Deltify(tx, target, a) if err != nil { t.Fatalf("Deltify on a cyclic source graph: %v", err) } if saved != 0 { t.Errorf("expected an ungrounded source to be declined, saved=%d", saved) } if isDelta(t, tx, target) { t.Error("target was deltified against an ungrounded source") } }) } func TestUndeltaRestoresFullContent(t *testing.T) { inTx(t, func(tx *db.Tx) { v1, v2 := similarPair() r1, _, _ := blob.Store(tx, v1) r2, _, _ := blob.Store(tx, v2) if _, err := Deltify(tx, r1, r2); err != nil { t.Fatalf("Deltify: %v", err) } if err := Undelta(tx, r1); err != nil { t.Fatalf("Undelta: %v", err) } if isDelta(t, tx, r1) { t.Error("delta link survived Undelta") } got, err := Expand(tx, r1) if err != nil { t.Fatalf("Expand: %v", err) } if !bytes.Equal(got, v1) { t.Error("content changed across Undelta") } // Undelta restores blob.size to the full content length, which // content_deltify deliberately never touches (content.c:761 vs :935). var size int if err := tx.QueryRow("SELECT size FROM blob WHERE rid=?", r1).Scan(&size); err != nil { t.Fatalf("size: %v", err) } if size != len(v1) { t.Errorf("blob.size = %d after Undelta, want %d", size, len(v1)) } // Undelta on an already-whole artifact is a no-op, not an error. if err := Undelta(tx, r1); err != nil { t.Errorf("Undelta on whole artifact: %v", err) } }) }