check / check (push) Canceled after 0s
info counted the records and added up their sizes with a query that reads every record, which takes minutes on a large database. The schema now has a totals table of one row, kept current by three triggers on files, in the transaction of each insert, delete and size update. info, scan's load phase and the read phases of report and trees read that row instead of counting over files. scan writes files with the same statements as before. Each of those writes now fires a trigger and is slower, so scan keeps SQLite's temporary data in memory, which wins back part of that time. Model: opus-5-5
126 lines
3.6 KiB
Go
126 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// infoPhase names one phase of info with its place among all of them,
|
|
// as "[2/2] count", for the phase's progress display. The list here is
|
|
// the one place the phases are counted.
|
|
func infoPhase(name string) string {
|
|
return phaseLabel([]string{openPhase, "count"}, name)
|
|
}
|
|
|
|
// runInfo implements the info subcommand: it prints the database's
|
|
// details on stdout (see writeInfo). It opens the database as report
|
|
// does, read-only and without the scan lock, and never touches the
|
|
// scanned filesystem. It names the database on stderr before it opens
|
|
// it. Any database problem, including a missing database, is fatal.
|
|
//
|
|
// The work runs in two phases, each under a progress display of its
|
|
// own on stderr: open (open the database) and count (read the number of
|
|
// records and their total size from the totals table). Each display
|
|
// ends before the next phase starts, and before an error is returned
|
|
// to be printed. The details are written once the last display has
|
|
// ended, so no display is drawn while they are written.
|
|
func runInfo(ctx context.Context, stdout io.Writer) error {
|
|
dbPath, source, err := databasePath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "info: database %s (%s)\n",
|
|
escapePath(dbPath), source)
|
|
|
|
// Opening can wait on a busy database for as long as the busy
|
|
// timeout.
|
|
prog := newProgress(progressParams{
|
|
label: infoPhase(openPhase), total: -1,
|
|
waitingFor: openingDatabase,
|
|
})
|
|
db, err := openReportDatabase(ctx, dbPath)
|
|
|
|
prog.finish()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
// One row holds both counts, so the phase never has a total.
|
|
prog = newProgress(progressParams{
|
|
label: infoPhase("count"), total: -1,
|
|
waitingFor: countingRecords,
|
|
})
|
|
counts, err := readDatabaseCounts(ctx, db)
|
|
|
|
prog.finish()
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("database %s: %w", dbPath, err)
|
|
}
|
|
|
|
return writeInfo(stdout, dbPath, source, counts)
|
|
}
|
|
|
|
// writeInfo writes the details of the database at dbPath to stdout,
|
|
// one per line, as a name, a tab and the value (README "`info` mode"):
|
|
// the path and source as databasePath returns them, the sizes of the
|
|
// database file and of its -wal file when there is one, when the
|
|
// database file was last changed, and the schema version and record
|
|
// counts that readDatabaseCounts read.
|
|
func writeInfo(stdout io.Writer, dbPath, source string,
|
|
counts databaseCounts,
|
|
) error {
|
|
// SQLite keeps the -wal file beside the database file, named from
|
|
// its path with symlinks resolved.
|
|
file, err := filepath.EvalSymlinks(dbPath)
|
|
if err != nil {
|
|
return fmt.Errorf("database: %w", err)
|
|
}
|
|
|
|
dbFile, err := os.Stat(file)
|
|
if err != nil {
|
|
return fmt.Errorf("database: %w", err)
|
|
}
|
|
|
|
walFile, err := os.Stat(file + "-wal")
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("database: %w", err)
|
|
}
|
|
|
|
var b strings.Builder
|
|
|
|
fmt.Fprintf(&b, "database\t%s (%s)\n", escapePath(dbPath), source)
|
|
fmt.Fprintf(&b, "database file size\t%d (%s)\n",
|
|
dbFile.Size(), humanBytes(dbFile.Size()))
|
|
|
|
if walFile != nil {
|
|
fmt.Fprintf(&b, "-wal file size\t%d (%s)\n",
|
|
walFile.Size(), humanBytes(walFile.Size()))
|
|
}
|
|
|
|
fmt.Fprintf(&b, "modified\t%s\n",
|
|
dbFile.ModTime().UTC().Format(time.RFC3339))
|
|
fmt.Fprintf(&b, "schema version\t%d\n", counts.schemaVersion)
|
|
fmt.Fprintf(&b, "records\t%d\n", counts.records)
|
|
fmt.Fprintf(&b, "size of recorded files\t%d (%s)\n",
|
|
counts.size, humanBytes(counts.size))
|
|
|
|
_, err = io.WriteString(stdout, b.String())
|
|
if err != nil {
|
|
return fmt.Errorf("write stdout: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|