Skip to content

Commit 9e3e816

Browse files
authored
Merge pull request #576 from SiaFoundation/nate/integrity-check
Add SQLite integrity check command
2 parents deb74a2 + ac5e97e commit 9e3e816

File tree

4 files changed

+97
-33
lines changed

4 files changed

+97
-33
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: patch
3+
---
4+
5+
# Added "sqlite integrity" command

cmd/hostd/main.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,13 @@ Perform various operations on the SQLite3 database.
225225
226226
Commands:
227227
backup Create a backup of the SQLite3 database
228+
integrity Check the integrity of the SQLite3 database
229+
`
230+
231+
sqlite3IntegrityUsage = `Usage:
232+
hostd sqlite3 integrity <srcPath>
233+
234+
Check the integrity of the SQLite3 database at the specified path. This is not safe to run while the host is running.
228235
`
229236

230237
sqlite3BackupUsage = `Usage:
@@ -256,6 +263,10 @@ func runBackupCommand(srcPath, destPath string) error {
256263
return nil
257264
}
258265

266+
func runIntegrityCommand(ctx context.Context, srcPath string, log *zap.Logger) error {
267+
return sqlite.IntegrityCheck(ctx, srcPath, log)
268+
}
269+
259270
func runRecalcCommand(srcPath string, log *zap.Logger) error {
260271
db, err := sqlite.OpenDatabase(srcPath, log)
261272
if err != nil {
@@ -313,6 +324,7 @@ func main() {
313324
recalculateCmd := flagg.New("recalculate", recalculateUsage)
314325
sqlite3Cmd := flagg.New("sqlite3", sqlite3Usage)
315326
sqlite3BackupCmd := flagg.New("backup", sqlite3BackupUsage)
327+
sqlite3IntegrityCmd := flagg.New("integrity", sqlite3IntegrityUsage)
316328

317329
cmd := flagg.Parse(flagg.Tree{
318330
Cmd: rootCmd,
@@ -324,6 +336,7 @@ func main() {
324336
{
325337
Cmd: sqlite3Cmd,
326338
Sub: []flagg.Tree{
339+
{Cmd: sqlite3IntegrityCmd},
327340
{Cmd: sqlite3BackupCmd},
328341
},
329342
},
@@ -368,6 +381,25 @@ func main() {
368381
checkFatalError("command failed", runRecalcCommand(cmd.Arg(0), log))
369382
case sqlite3Cmd:
370383
cmd.Usage()
384+
case sqlite3IntegrityCmd:
385+
if len(cmd.Args()) != 1 {
386+
cmd.Usage()
387+
return
388+
}
389+
390+
log := initStdoutLog(cfg.Log.StdOut.EnableANSI, "info")
391+
defer log.Sync()
392+
393+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
394+
defer cancel()
395+
396+
log.Info("running integrity check")
397+
checkFatalError("integrity check failed", runIntegrityCommand(ctx, cmd.Arg(0), log))
398+
log.Info("integrity check passed")
399+
400+
log.Info("running foreign key check")
401+
checkFatalError("foreign key check failed", sqlite.ForeignKeyCheck(ctx, cmd.Arg(0), log))
402+
log.Info("foreign key check passed")
371403
case sqlite3BackupCmd:
372404
if len(cmd.Args()) != 2 {
373405
cmd.Usage()

persist/sqlite/init.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package sqlite
22

33
import (
4+
"database/sql"
45
_ "embed" // for init.sql
6+
"errors"
57
"time"
68

79
"fmt"
@@ -41,7 +43,7 @@ func (s *Store) upgradeDatabase(current, target int64) error {
4143
return fmt.Errorf("failed to enable foreign key deferral: %w", err)
4244
} else if err := fn(tx, log); err != nil {
4345
return err
44-
} else if err := foreignKeyCheck(tx, log); err != nil {
46+
} else if err := checkFKConsistency(tx, log); err != nil {
4547
return fmt.Errorf("failed foreign key check: %w", err)
4648
}
4749
return setDBVersion(tx, version)
@@ -75,6 +77,33 @@ func (s *Store) init() error {
7577
return nil
7678
}
7779

80+
func checkFKConsistency(txn *txn, log *zap.Logger) error {
81+
rows, err := txn.Query("PRAGMA foreign_key_check")
82+
if err != nil {
83+
return fmt.Errorf("failed to run foreign key check: %w", err)
84+
}
85+
defer rows.Close()
86+
var hasErrors bool
87+
for rows.Next() {
88+
var table string
89+
var rowid sql.NullInt64
90+
var fkTable string
91+
var fkRowid sql.NullInt64
92+
93+
if err := rows.Scan(&table, &rowid, &fkTable, &fkRowid); err != nil {
94+
return fmt.Errorf("failed to scan foreign key check result: %w", err)
95+
}
96+
hasErrors = true
97+
log.Error("foreign key constraint violated", zap.String("table", table), zap.Int64("rowid", rowid.Int64), zap.String("fkTable", fkTable), zap.Int64("fkRowid", fkRowid.Int64))
98+
}
99+
if err := rows.Err(); err != nil {
100+
return fmt.Errorf("failed to iterate foreign key check results: %w", err)
101+
} else if hasErrors {
102+
return errors.New("foreign key constraint violated")
103+
}
104+
return nil
105+
}
106+
78107
func generateHostKey(tx *txn) (err error) {
79108
key := types.NewPrivateKeyFromSeed(frand.Bytes(32))
80109
var dbID int64

persist/sqlite/store.go

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -234,14 +234,28 @@ func Backup(ctx context.Context, srcPath, destPath string) (err error) {
234234
return backupDB(ctx, src, destPath)
235235
}
236236

237-
func integrityCheck(db *sql.DB, log *zap.Logger) error {
238-
rows, err := db.Query("PRAGMA integrity_check")
237+
// IntegrityCheck runs a PRAGMA integrity_check on the database and logs any
238+
// integrity errors. If any errors are found, an error is returned.
239+
func IntegrityCheck(ctx context.Context, fp string, log *zap.Logger) error {
240+
db, err := sql.Open("sqlite3", sqliteFilepath(fp))
241+
if err != nil {
242+
return fmt.Errorf("failed to open database: %w", err)
243+
}
244+
defer db.Close()
245+
246+
rows, err := db.QueryContext(ctx, "PRAGMA integrity_check")
239247
if err != nil {
240248
return fmt.Errorf("failed to run integrity check: %w", err)
241249
}
242250
defer rows.Close()
243251
var hasErrors bool
244252
for rows.Next() {
253+
select {
254+
case <-ctx.Done():
255+
return ctx.Err()
256+
default:
257+
}
258+
245259
var result string
246260
if err := rows.Scan(&result); err != nil {
247261
return fmt.Errorf("failed to scan integrity check result: %w", err)
@@ -258,41 +272,29 @@ func integrityCheck(db *sql.DB, log *zap.Logger) error {
258272
return nil
259273
}
260274

261-
func dbForeignKeyCheck(db *sql.DB, log *zap.Logger) error {
262-
rows, err := db.Query("PRAGMA foreign_key_check")
275+
// ForeignKeyCheck runs a PRAGMA foreign_key_check on the database and logs any
276+
// foreign key constraint violations. If any violations are found, an error is
277+
// returned.
278+
func ForeignKeyCheck(ctx context.Context, fp string, log *zap.Logger) error {
279+
db, err := sql.Open("sqlite3", sqliteFilepath(fp))
263280
if err != nil {
264-
return fmt.Errorf("failed to run foreign key check: %w", err)
281+
return fmt.Errorf("failed to open database: %w", err)
265282
}
266-
defer rows.Close()
267-
var hasErrors bool
268-
for rows.Next() {
269-
var table string
270-
var rowid sql.NullInt64
271-
var fkTable string
272-
var fkRowid sql.NullInt64
283+
defer db.Close()
273284

274-
if err := rows.Scan(&table, &rowid, &fkTable, &fkRowid); err != nil {
275-
return fmt.Errorf("failed to scan foreign key check result: %w", err)
276-
}
277-
hasErrors = true
278-
log.Error("foreign key constraint violated", zap.String("table", table), zap.Int64("rowid", rowid.Int64), zap.String("fkTable", fkTable), zap.Int64("fkRowid", fkRowid.Int64))
279-
}
280-
if err := rows.Err(); err != nil {
281-
return fmt.Errorf("failed to iterate foreign key check results: %w", err)
282-
} else if hasErrors {
283-
return errors.New("foreign key constraint violated")
284-
}
285-
return nil
286-
}
287-
288-
func foreignKeyCheck(txn *txn, log *zap.Logger) error {
289-
rows, err := txn.Query("PRAGMA foreign_key_check")
285+
rows, err := db.QueryContext(ctx, "PRAGMA foreign_key_check")
290286
if err != nil {
291287
return fmt.Errorf("failed to run foreign key check: %w", err)
292288
}
293289
defer rows.Close()
294290
var hasErrors bool
295291
for rows.Next() {
292+
select {
293+
case <-ctx.Done():
294+
return ctx.Err()
295+
default:
296+
}
297+
296298
var table string
297299
var rowid sql.NullInt64
298300
var fkTable string
@@ -325,10 +327,6 @@ func OpenDatabase(fp string, log *zap.Logger) (*Store, error) {
325327
}
326328
if err := store.init(); err != nil {
327329
return nil, err
328-
} else if err := dbForeignKeyCheck(db, log.Named("foreignkeys")); err != nil {
329-
return nil, fmt.Errorf("foreign key check failed: %w", err)
330-
} else if err := integrityCheck(db, log.Named("integrity")); err != nil {
331-
return nil, fmt.Errorf("integrity check failed: %w", err)
332330
}
333331
sqliteVersion, _, _ := sqlite3.Version()
334332
log.Debug("database initialized", zap.String("sqliteVersion", sqliteVersion), zap.Int("schemaVersion", len(migrations)+1), zap.String("path", fp))

0 commit comments

Comments
 (0)