Artifact 625357fa4caa928f9a3349ee83c2c8835445d6a0774b7def11d113a17abb3583:
- File
internal/blob/blob.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: 12203) [annotate] [blame] [check-ins using]
package blob import ( "fmt" libfossil "github.com/danmestas/libfossil/internal/fsltype" "github.com/danmestas/libfossil/db" "github.com/danmestas/libfossil/internal/delta" "github.com/danmestas/libfossil/internal/hash" ) func Store(q db.Querier, content []byte) (rid libfossil.FslID, uuid string, err error) { if q == nil { panic("blob.Store: q must not be nil") } if len(content) == 0 { panic("blob.Store: content length must be > 0") } defer func() { if err == nil && rid <= 0 { panic("blob.Store: postcondition violated: rid <= 0 with no error") } }() uuid = hash.SHA1(content) if rid, ok := Exists(q, uuid); ok { return rid, uuid, nil } compressed, err := Compress(content) if err != nil { return 0, "", fmt.Errorf("blob.Store compress: %w", err) } result, err := q.Exec( "INSERT INTO blob(uuid, size, content, rcvid) VALUES(?, ?, ?, 1)", uuid, len(content), compressed, ) if err != nil { return 0, "", fmt.Errorf("blob.Store insert: %w", err) } ridInt, err := result.LastInsertId() if err != nil { return 0, "", fmt.Errorf("blob.Store lastid: %w", err) } rid = libfossil.FslID(ridInt) // Verify round-trip: re-read, decompress, re-hash. // Matches Fossil's content_put_pk() post-write verification. readBack, err := Load(q, rid) if err != nil { return 0, "", fmt.Errorf("blob.Store verify read-back: %w", err) } if got := hash.SHA1(readBack); got != uuid { return 0, "", fmt.Errorf("blob.Store verify: hash mismatch after round-trip: stored %s, got %s", uuid, got) } // Mark as unclustered — matches Fossil's content_put_ex (content.c:633). // Only new blobs reach here; Exists early-return skips this. if _, err := q.Exec("INSERT OR IGNORE INTO unclustered(rid) VALUES(?)", rid); err != nil { return 0, "", fmt.Errorf("blob.Store unclustered: %w", err) } return rid, uuid, nil } // StoreDelta inserts a NEW row holding content delta-encoded against // srcRid. It has no callers outside tests and is not what the commit path // uses: deltifying an already-stored artifact rewrites an existing row // rather than inserting one, and the decision of what to deltify is // content.Deltify's, which holds the whole policy in one place. Kept for // the delta-chain fixtures several packages build with it. func StoreDelta(q db.Querier, content []byte, srcRid libfossil.FslID) (rid libfossil.FslID, uuid string, err error) { if q == nil { panic("blob.StoreDelta: q must not be nil") } if len(content) == 0 { panic("blob.StoreDelta: content length must be > 0") } if srcRid <= 0 { panic("blob.StoreDelta: srcRid must be > 0") } defer func() { if err == nil && rid <= 0 { panic("blob.StoreDelta: postcondition violated: rid <= 0 with no error") } }() uuid = hash.SHA1(content) if rid, ok := Exists(q, uuid); ok { return rid, uuid, nil } srcContent, err := Load(q, srcRid) if err != nil { return 0, "", fmt.Errorf("blob.StoreDelta load source: %w", err) } deltaBytes := delta.Create(srcContent, content) compressed, err := Compress(deltaBytes) if err != nil { return 0, "", fmt.Errorf("blob.StoreDelta compress: %w", err) } result, err := q.Exec( "INSERT INTO blob(uuid, size, content, rcvid) VALUES(?, ?, ?, 1)", uuid, len(content), compressed, ) if err != nil { return 0, "", fmt.Errorf("blob.StoreDelta insert blob: %w", err) } ridInt, err := result.LastInsertId() if err != nil { return 0, "", fmt.Errorf("blob.StoreDelta lastid: %w", err) } rid = libfossil.FslID(ridInt) _, err = q.Exec("INSERT INTO delta(rid, srcid) VALUES(?, ?)", rid, srcRid) if err != nil { return 0, "", fmt.Errorf("blob.StoreDelta insert delta: %w", err) } // Verify round-trip: re-read delta, apply to source, re-hash. // Matches Fossil's content_put_pk() post-write verification. storedDelta, err := Load(q, rid) if err != nil { return 0, "", fmt.Errorf("blob.StoreDelta verify read-back: %w", err) } rebuilt, err := delta.Apply(srcContent, storedDelta) if err != nil { return 0, "", fmt.Errorf("blob.StoreDelta verify apply: %w", err) } if got := hash.SHA1(rebuilt); got != uuid { return 0, "", fmt.Errorf("blob.StoreDelta verify: hash mismatch after round-trip: stored %s, got %s", uuid, got) } // Mark as unclustered — matches Fossil's content_put_ex (content.c:633). if _, err := q.Exec("INSERT OR IGNORE INTO unclustered(rid) VALUES(?)", rid); err != nil { return 0, "", fmt.Errorf("blob.StoreDelta unclustered: %w", err) } return rid, uuid, nil } // StoreDeltaRaw stores delta-encoded content exactly as given — without // expanding it against its source — and records the delta-to-source link. // Unlike StoreDelta, which computes a delta from full target content and // verifies the round-trip against an already-readable source, StoreDeltaRaw // accepts content that arrived already delta-encoded (e.g. over the wire // during a transfer) whose source may itself still be a phantom. The // target's size is read from the delta's own header (delta.OutputSize), so // no source content is needed to store it. // // If a real (non-phantom) blob already exists for uuid, this is a no-op. // If a phantom row exists, it is filled in place. Otherwise a new row is // created. In every case the row's declared size is real, never -1: the // target is not phantomized just because its source might be — mirrors // Fossil's content_put_ex (src/content.c:557-620), which stores delta // content unconditionally and records REPLACE INTO delta(rid,srcid) // whether or not the source is currently available. Availability is a // live, transitively-recomputed property (content.IsAvailable), not // something this function or any caller needs to update or be notified // about; content.Expand verifies the expanded result against its claimed // UUID on every read, since nothing here can verify a delta whose source // isn't readable yet. // // deltaBytes is wire data, not a programmer-controlled argument: malformed // or empty input (including from a hostile peer) is reported as an error, // never a panic. Argument shapes StoreDeltaRaw's own caller controls (q, // uuid, srcRid) are still asserted, matching this package's usual // nil/invalid-argument convention. func StoreDeltaRaw(q db.Querier, uuid string, deltaBytes []byte, srcRid libfossil.FslID) (rid libfossil.FslID, err error) { if q == nil { panic("blob.StoreDeltaRaw: q must not be nil") } if uuid == "" { panic("blob.StoreDeltaRaw: uuid must not be empty") } if srcRid <= 0 { panic("blob.StoreDeltaRaw: srcRid must be > 0") } defer func() { if err == nil && rid <= 0 { panic("blob.StoreDeltaRaw: postcondition violated: rid <= 0 with no error") } }() existingRid, exists := Exists(q, uuid) if exists { var size int64 if err := q.QueryRow("SELECT size FROM blob WHERE rid=?", existingRid).Scan(&size); err != nil { return 0, fmt.Errorf("blob.StoreDeltaRaw: check existing: %w", err) } if size != -1 { return existingRid, nil // real blob already exists; nothing to do } } // delta.OutputSize rejects empty, malformed, and overflowing/oversized // headers with an error — the boundary for wire-data validation lives // there so this function doesn't duplicate it. targetSize, err := delta.OutputSize(deltaBytes) if err != nil { return 0, fmt.Errorf("blob.StoreDeltaRaw: output size: %w", err) } compressed, err := Compress(deltaBytes) if err != nil { return 0, fmt.Errorf("blob.StoreDeltaRaw: compress: %w", err) } if exists { // Filling a phantom. if _, err := q.Exec("UPDATE blob SET rcvid=1, size=?, content=? WHERE rid=?", int64(targetSize), compressed, existingRid); err != nil { return 0, fmt.Errorf("blob.StoreDeltaRaw: fill phantom: %w", err) } if _, err := q.Exec("DELETE FROM phantom WHERE rid=?", existingRid); err != nil { return 0, fmt.Errorf("blob.StoreDeltaRaw: clear phantom: %w", err) } rid = existingRid } else { result, err := q.Exec( "INSERT INTO blob(uuid, size, content, rcvid) VALUES(?, ?, ?, 1)", uuid, int64(targetSize), compressed, ) if err != nil { return 0, fmt.Errorf("blob.StoreDeltaRaw: insert: %w", err) } ridInt, err := result.LastInsertId() if err != nil { return 0, fmt.Errorf("blob.StoreDeltaRaw: lastid: %w", err) } rid = libfossil.FslID(ridInt) } // Record the delta-to-source link unconditionally — matches // content_put_ex's REPLACE INTO delta(rid,srcid), which fires // regardless of whether srcRid is itself currently available. if _, err := q.Exec("REPLACE INTO delta(rid, srcid) VALUES(?, ?)", rid, srcRid); err != nil { return 0, fmt.Errorf("blob.StoreDeltaRaw: delta link: %w", err) } if _, err := q.Exec("INSERT OR IGNORE INTO unclustered(rid) VALUES(?)", rid); err != nil { return 0, fmt.Errorf("blob.StoreDeltaRaw: unclustered: %w", err) } return rid, nil } func StorePhantom(q db.Querier, uuid string) (rid libfossil.FslID, err error) { if q == nil { panic("blob.StorePhantom: q must not be nil") } if uuid == "" { panic("blob.StorePhantom: uuid must not be empty") } defer func() { if err == nil && rid <= 0 { panic("blob.StorePhantom: postcondition violated: rid <= 0 with no error") } }() if rid, ok := Exists(q, uuid); ok { return rid, nil } result, err := q.Exec( "INSERT INTO blob(uuid, size, content, rcvid) VALUES(?, -1, NULL, 0)", uuid, ) if err != nil { return 0, fmt.Errorf("blob.StorePhantom: %w", err) } ridInt, err := result.LastInsertId() if err != nil { return 0, fmt.Errorf("blob.StorePhantom lastid: %w", err) } rid = libfossil.FslID(ridInt) _, err = q.Exec("INSERT INTO phantom(rid) VALUES(?)", rid) if err != nil { return 0, fmt.Errorf("blob.StorePhantom phantom table: %w", err) } return rid, nil } func Load(q db.Querier, rid libfossil.FslID) (result []byte, err error) { if q == nil { panic("blob.Load: q must not be nil") } if rid <= 0 { panic("blob.Load: rid must be > 0") } defer func() { if err == nil && result == nil { panic("blob.Load: postcondition violated: result is nil with no error") } }() var content []byte var size int64 err = q.QueryRow("SELECT content, size FROM blob WHERE rid=?", rid).Scan(&content, &size) if err != nil { return nil, fmt.Errorf("blob.Load query: %w", err) } if size == -1 { return nil, fmt.Errorf("blob.Load: rid %d is a phantom", rid) } if content == nil || len(content) == 0 { return nil, fmt.Errorf("blob.Load: rid %d has NULL or empty content", rid) } // Fossil stores blobs as [4-byte BE uncompressed-size][zlib data]. // When the compressed form happens to be the same length as the // uncompressed content (rare but real — ~2 in 66K in the Fossil SCM // repo), we must still decompress. Detect compressed content by // checking for the 4-byte BE prefix matching the declared size // followed by a zlib header (0x78). if len(content) >= 6 { prefixSize := int64(content[0])<<24 | int64(content[1])<<16 | int64(content[2])<<8 | int64(content[3]) if prefixSize == size && content[4] == 0x78 { return Decompress(content) } } // No compression prefix — content is stored uncompressed. if int64(len(content)) == size { return content, nil } // Stored bytes < declared size — compressed. return Decompress(content) } // Exists reports whether a blob row with the given uuid is present, and // returns its rid. It answers existence only. // // It returns true for a phantom — a real blob row with size = -1 and NULL // content, standing in for an artifact we know of but have not received. // A phantom's content cannot be read, and neither can that of a delta whose // chain bottoms out in one. Callers that are about to read content must use // content.AvailableByUUID instead, which is transitive over the delta chain. // // Mirrors Fossil's rid_from_uuid (src/xfer.c:70). func Exists(q db.Querier, uuid string) (libfossil.FslID, bool) { if q == nil { panic("blob.Exists: q must not be nil") } if uuid == "" { panic("blob.Exists: uuid must not be empty") } var rid int64 err := q.QueryRow("SELECT rid FROM blob WHERE uuid=?", uuid).Scan(&rid) if err != nil { return 0, false } return libfossil.FslID(rid), true }