Add sfdupes info: show the database's details (closes #117)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
info prints the database's path and source, the sizes of the database file and its -wal file, when the database file last changed, the schema version and the record counts, one per line as a name, a tab and the value. It opens the database as report does and shows an open and a count phase on stderr. The schema version and the counts come from one read-only transaction, the counts from one query. seedDatabase now closes test databases out of WAL mode, as scan does; otherwise info's own connection creates an empty -wal file. The open phase's name and text became constants for the linter, and the scan command moved to newScanCommand to keep newRootCommand short enough. Judgement call: the lines are written after the last display ends, so they skip the display's stdout writer. Model: opus-5-5
This commit was merged in pull request #123.
This commit is contained in:
@@ -79,7 +79,8 @@ ON CONFLICT (path) DO UPDATE SET
|
||||
content = excluded.content
|
||||
`
|
||||
|
||||
// errNoDatabase reports a missing database file for report/trees.
|
||||
// errNoDatabase reports a missing database file for report, trees and
|
||||
// info.
|
||||
var errNoDatabase = errors.New(
|
||||
"no database (run \"sfdupes scan\" first, or set " + databaseEnv + ")")
|
||||
|
||||
@@ -131,9 +132,9 @@ const scanParams = "_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=journal_mode(WAL)" +
|
||||
"&_pragma=synchronous(NORMAL)"
|
||||
|
||||
// reportParams are the connection parameters for report and trees:
|
||||
// read-only, with the same busy timeout. They set no journal mode,
|
||||
// because setting one is a write.
|
||||
// reportParams are the connection parameters for report, trees and
|
||||
// info: read-only, with the same busy timeout. They set no journal
|
||||
// mode, because setting one is a write.
|
||||
const reportParams = "mode=ro" +
|
||||
"&_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=query_only(1)"
|
||||
@@ -262,9 +263,9 @@ func closeScanDatabase(ctx context.Context, db *sql.DB, path string) {
|
||||
_ = db.Close()
|
||||
}
|
||||
|
||||
// openReportDatabase opens an existing database for the report and
|
||||
// trees subcommands. A missing database file is an error directing the
|
||||
// user to run scan first; the schema version must match exactly.
|
||||
// openReportDatabase opens an existing database for the report, trees
|
||||
// and info subcommands. A missing database file is an error directing
|
||||
// the user to run scan first; the schema version must match exactly.
|
||||
func openReportDatabase(ctx context.Context,
|
||||
path string,
|
||||
) (*sql.DB, error) {
|
||||
@@ -545,6 +546,60 @@ func countRecords(ctx context.Context, db *sql.DB) (int64, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// databaseCountsSQL counts the records, those with head and tail
|
||||
// hashes and those with a content hash, and adds up their sizes, in one
|
||||
// pass over the table. A SUM over no rows is NULL, so on an empty table
|
||||
// each SUM falls back to 0.
|
||||
const databaseCountsSQL = `
|
||||
SELECT COUNT(*),
|
||||
COALESCE(SUM(head <> '' AND tail <> ''), 0),
|
||||
COALESCE(SUM(content <> ''), 0),
|
||||
COALESCE(SUM(size), 0)
|
||||
FROM files
|
||||
`
|
||||
|
||||
// databaseCounts is what info reads from the database: its schema
|
||||
// version and the counts of databaseCountsSQL.
|
||||
type databaseCounts struct {
|
||||
schemaVersion int
|
||||
records int64 // every record
|
||||
headTail int64 // records with head and tail hashes
|
||||
content int64 // records with a content hash
|
||||
size int64 // the sizes of all records added up
|
||||
}
|
||||
|
||||
// readDatabaseCounts reads the schema version, then the counts of
|
||||
// databaseCountsSQL, in one read-only transaction, so both describe the
|
||||
// database at one moment while a scan is committing.
|
||||
func readDatabaseCounts(ctx context.Context,
|
||||
db *sql.DB,
|
||||
) (databaseCounts, error) {
|
||||
var c databaseCounts
|
||||
|
||||
// Everything goes through tx: the report connection is the only
|
||||
// one, so a query on db would wait for tx forever.
|
||||
tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("count records: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
err = tx.QueryRowContext(ctx, "PRAGMA user_version").
|
||||
Scan(&c.schemaVersion)
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("read schema version: %w", err)
|
||||
}
|
||||
|
||||
err = tx.QueryRowContext(ctx, databaseCountsSQL).
|
||||
Scan(&c.records, &c.headTail, &c.content, &c.size)
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("count records: %w", err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// loadFileMeta streams every record's path, size, mtime, and whether
|
||||
// it carries hashes to fn. Scan change detection needs no hash
|
||||
// values, and skipping the hash columns keeps the scan's in-memory
|
||||
|
||||
Reference in New Issue
Block a user