report: show its phases as N of M, with progress, elapsed time and ETA (closes #114)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
report now runs in two phases, open and read, under the progress display scan and trees use, until the summary line. open covers a wait on a busy database. read counts the records, then reads them in the same read-only transaction in signature order, the files_signature index read backwards, so SQLite sorts nothing and the read ends at exactly its count. The duplicate groups are formed in Go from runs of equal signatures; only those of the size being read are held, and they are written, ordered by first path, before the next size. The rows go through the display's stdout writer. stdout and the summary line are unchanged. Judgement call: writing is part of read, not a phase of its own, since each size's rows are written while the read goes on. Model: opus-5-5
This commit was merged in pull request #122.
This commit is contained in:
+96
-31
@@ -291,53 +291,118 @@ func TestCloseScanDatabaseWhileReportOpen(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadFileRowsCountAgreesDuringScan has a scan commit records after
|
||||
// loadFileRows has counted the records and before it reads them, as
|
||||
// when trees runs during a scan: it must read exactly as many records
|
||||
// as it counted.
|
||||
func TestLoadFileRowsCountAgreesDuringScan(t *testing.T) {
|
||||
// TestLoadRowsCountAgreeDuringScan has a scan commit records after
|
||||
// loadFileRows, as trees calls it, or loadSignatureRows, as report
|
||||
// calls it, has counted the records and before it reads them, as when
|
||||
// trees or report runs during a scan: each must read exactly as many
|
||||
// records as it counted.
|
||||
func TestLoadRowsCountAgreeDuringScan(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
path := testDBPath(t)
|
||||
loads := map[string]func(context.Context, *sql.DB, func(int64),
|
||||
func(scanRec)) error{
|
||||
"loadFileRows": loadFileRows,
|
||||
"loadSignatureRows": func(ctx context.Context, db *sql.DB,
|
||||
total func(int64), fn func(scanRec),
|
||||
) error {
|
||||
return loadSignatureRows(ctx, db, total, func(r scanRec) error {
|
||||
fn(r)
|
||||
|
||||
scanDB, err := openScanDatabase(t.Context(), path)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
for name, load := range loads {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
path := testDBPath(t)
|
||||
|
||||
scanDB, err := openScanDatabase(t.Context(), path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() { _ = scanDB.Close() }()
|
||||
|
||||
recs := smokeTreeRecs()
|
||||
|
||||
err = applyChanges(t.Context(), scanDB, recs[:3], nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reportDB, err := openReportDatabase(t.Context(), path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() { _ = reportDB.Close() }()
|
||||
|
||||
var counted, read int64
|
||||
|
||||
err = load(t.Context(), reportDB, func(n int64) {
|
||||
counted = n
|
||||
|
||||
err := applyChanges(t.Context(), scanDB, recs[3:], nil, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}, func(scanRec) { read++ })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if counted != 3 || read != counted {
|
||||
t.Errorf("counted %d records and read %d, want 3 and 3",
|
||||
counted, read)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignatureRowsNeedNoSort checks that SQLite reads signatureRowsSQL
|
||||
// from the files_signature index alone, with nothing to sort: the
|
||||
// first record comes at once, so report's read phase counts each
|
||||
// record as it comes rather than waiting on SQLite for all of them.
|
||||
func TestSignatureRowsNeedNoSort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := openTestDB(t)
|
||||
|
||||
rows, err := db.QueryContext(t.Context(),
|
||||
"EXPLAIN QUERY PLAN "+signatureRowsSQL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() { _ = scanDB.Close() }()
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
recs := smokeTreeRecs()
|
||||
var plan []string
|
||||
|
||||
err = applyChanges(t.Context(), scanDB, recs[:3], nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, parent, unused int
|
||||
detail string
|
||||
)
|
||||
|
||||
reportDB, err := openReportDatabase(t.Context(), path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() { _ = reportDB.Close() }()
|
||||
|
||||
var counted, read int64
|
||||
|
||||
err = loadFileRows(t.Context(), reportDB, func(n int64) {
|
||||
counted = n
|
||||
|
||||
err := applyChanges(t.Context(), scanDB, recs[3:], nil, nil)
|
||||
err = rows.Scan(&id, &parent, &unused, &detail)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
}, func(scanRec) { read++ })
|
||||
|
||||
plan = append(plan, detail)
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if counted != 3 || read != counted {
|
||||
t.Errorf("counted %d records and read %d, want 3 and 3",
|
||||
counted, read)
|
||||
want := []string{"SCAN files USING COVERING INDEX files_signature"}
|
||||
if !slices.Equal(plan, want) {
|
||||
t.Errorf("query plan = %q, want %q", plan, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user