Artifact 1fe3ddcb6dccea316751f3fe91e3412d631cb7cda1ea1496a7c532ce96618ad9:
- File
internal/manifest/receipt_test.go
— part of check-in
[0269bd9461]
at
2026-10-19 14:52:58
on branch feat/submanifest-reader
— fix(db): forget a receipt's row when its transaction rolls back
Receipt.ID took (rcvid, mtime) to identify its own row and checked it with a SELECT on every use. After a rollback the rcvid can be reused by another receipt's insert, and under a frozen clock the mtime matches too, so the receipt attached to the wrong row.
Fossil holds g.rcvid for its process and never re-reads the row (content_rcvid_init); a rollback ends the process with it. A Receipt outlives transactions, so the transaction or savepoint that makes its row now records that, forgets the id if it rolls back, and hands it to the enclosing level when released. A kept id costs no query, which takes one SELECT per stored blob off the clone path.
Also checks that a tree check-in's sub-manifests share the commit's receipt. (user: [email protected], size: 3310) [annotate] [blame] [check-ins using]
package manifest import ( "testing" "github.com/danmestas/go-libfossil/db" ) // receiptRow is one rcvfrom row with the number of blobs naming it. type receiptRow struct { rcvid, uid, blobs int64 nonceNull, ipNull bool } func receiptRows(t *testing.T, q db.Querier) []receiptRow { t.Helper() rows, err := q.Query("SELECT rcvid, uid, nonce IS NULL, ipaddr IS NULL," + " (SELECT count(*) FROM blob WHERE blob.rcvid=rcvfrom.rcvid)" + " FROM rcvfrom ORDER BY rcvid") if err != nil { t.Fatalf("query rcvfrom: %v", err) } defer rows.Close() var out []receiptRow for rows.Next() { var r receiptRow if err := rows.Scan(&r.rcvid, &r.uid, &r.nonceNull, &r.ipNull, &r.blobs); err != nil { t.Fatalf("scan rcvfrom: %v", err) } out = append(out, r) } if err := rows.Err(); err != nil { t.Fatalf("rcvfrom rows: %v", err) } return out } // Each check-in is one receipt, as each fossil commit is: one rcvfrom row, // credited to the committing user with no nonce or address, named by the // files it stored and its manifest. func TestCheckinRecordsOneReceipt(t *testing.T) { r := setupTestRepo(t) rid1, _, err := Checkin(r, CheckinOpts{ Comment: "one", User: "testuser", Files: []File{{Name: "a.txt", Content: []byte("a\n")}, {Name: "b.txt", Content: []byte("b\n")}}, }) if err != nil { t.Fatalf("Checkin one: %v", err) } if _, _, err := Checkin(r, CheckinOpts{ Comment: "two", User: "testuser", Parent: rid1, Files: []File{{Name: "a.txt", Content: []byte("a\n")}, {Name: "b.txt", Content: []byte("b2\n")}}, }); err != nil { t.Fatalf("Checkin two: %v", err) } want := []receiptRow{ {rcvid: 1, uid: 1, blobs: 3, nonceNull: true, ipNull: true}, {rcvid: 2, uid: 1, blobs: 2, nonceNull: true, ipNull: true}, } got := receiptRows(t, r.DB()) if len(got) != len(want) { t.Fatalf("rcvfrom rows = %+v, want %+v", got, want) } for i := range want { if got[i] != want[i] { t.Errorf("rcvfrom row %d = %+v, want %+v", i, got[i], want[i]) } } } // A tree check-in's sub-manifests are stored by the commit that stores its // manifest, so they name the commit's receipt, not one of their own. func TestTreeCheckinSubmanifestsShareTheReceipt(t *testing.T) { r := sha3Repo(t) commit(t, r, CheckinOpts{Files: []File{{Name: "first.txt", Content: []byte("1\n")}}, Comment: "an earlier receipt"}) setSettings(t, r, SettingEnableSubmanifests, "on", SettingSubmanifestK, "3") rid, d := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "tree"}) if len(d.S) == 0 { t.Fatal("fixture: the check-in carries no S card") } rcvidOf := func(where string, arg any) int64 { t.Helper() var id int64 if err := r.DB().QueryRow("SELECT rcvid FROM blob WHERE "+where, arg).Scan(&id); err != nil { t.Fatalf("rcvid where %s: %v", where, err) } return id } want := rcvidOf("rid=?", rid) if want <= 1 { t.Fatalf("manifest rcvid = %d, want the tree commit's own receipt", want) } for _, s := range d.S { if got := rcvidOf("uuid=?", s.UUID); got != want { t.Errorf("sub-manifest %s rcvid = %d, want the commit's %d", s.UUID[:10], got, want) } } var rows int if err := r.DB().QueryRow("SELECT count(*) FROM rcvfrom").Scan(&rows); err != nil { t.Fatal(err) } if rows != 2 { t.Fatalf("rcvfrom rows = %d, want 2, one per commit", rows) } }