Compute the content hash only when head and tail match (closes #61)
check / check (push) Successful in 1m9s

A file of 10 MiB or more now gets only its head and tail in the hash
phase. A new content phase, after the update phase, finds every record
of that size without a content hash whose size, head and tail match
another record's, anywhere in the database, checks each file with
lstat, and reads a group only while at least two members remain. It
reuses the hash worker pool, now given its hash function. report and
trees leave out records without a content hash. The README, help text
and TODO entry describe the gate; the schema stays at version 1.

Model: opus-5-5
This commit is contained in:
2026-09-23 12:18:39 +00:00
parent 09a39ddf37
commit 89fc9e4595
12 changed files with 763 additions and 221 deletions
+58
View File
@@ -278,6 +278,64 @@ func loadFileMeta(ctx context.Context, db *sql.DB,
return nil
}
// contentCandidatesSQL selects every record of at least headTailMin
// bytes that has no content hash but whose size, head, and tail equal
// another record's, with the number of records in its group (the
// records sharing that size, head, and tail) that already have a
// content hash. SQLite does the grouping, so no other record's hashes
// are loaded into memory; the rows come ordered by size, head, and
// tail, so each group's rows arrive together.
const contentCandidatesSQL = `
SELECT f.path, f.size, f.mtime, f.head, f.tail, g.hashed
FROM files AS f
JOIN (
SELECT size, head, tail, SUM(content <> '') AS hashed
FROM files
WHERE size >= ? AND head <> ''
GROUP BY size, head, tail
HAVING COUNT(*) > 1
) AS g USING (size, head, tail)
WHERE f.content = ''
ORDER BY size, head, tail
`
// loadContentCandidates streams the rows of contentCandidatesSQL to fn:
// each record, without its content hash, and the number of records in
// its group that already have one.
func loadContentCandidates(ctx context.Context, db *sql.DB,
fn func(r scanRec, hashed int),
) error {
rows, err := db.QueryContext(ctx, contentCandidatesSQL, headTailMin)
if err != nil {
return fmt.Errorf("read records: %w", err)
}
defer func() { _ = rows.Close() }()
for rows.Next() {
var (
path []byte
r scanRec
hashed int
)
err = rows.Scan(&path, &r.size, &r.mtime, &r.head, &r.tail, &hashed)
if err != nil {
return fmt.Errorf("read record: %w", err)
}
r.path = string(path)
fn(r, hashed)
}
err = rows.Err()
if err != nil {
return fmt.Errorf("read records: %w", err)
}
return nil
}
// updateBatchSize is the number of record changes committed per
// transaction during the update pass. The filesystem is authoritative
// and the database an eventually-consistent reflection of it, so