Skip to content

Commit

Permalink
squibble: rework how digests are verified
Browse files Browse the repository at this point in the history
- Rename SchemaHash to SQLDigest
- Rename DBHash to DBDigest
- Make sure history gets updated properly
  • Loading branch information
creachadair committed Feb 16, 2024
1 parent 0690634 commit 526ba6f
Show file tree
Hide file tree
Showing 4 changed files with 30 additions and 45 deletions.
4 changes: 2 additions & 2 deletions cmd/squibble/squibble.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func runDigest(env *command.Env, path string) error {
if err != nil {
return err
}
hash, err := squibble.SchemaHash(string(text))
hash, err := squibble.SQLDigest(string(text))
if err != nil {
return err
}
Expand All @@ -72,7 +72,7 @@ func runDigest(env *command.Env, path string) error {
}
defer db.Close()

hash, err := squibble.DBHash(context.Background(), db, "main")
hash, err := squibble.DBDigest(context.Background(), db, "main")
if err != nil {
return err
}
Expand Down
42 changes: 21 additions & 21 deletions squibble.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,35 +155,29 @@ func (s *Schema) Apply(ctx context.Context, db *sql.DB) error {
}

// Stage 2: Check whether the schema is up-to-date.
curHash, err := SchemaHash(s.Current)
curHash, err := SQLDigest(s.Current)
if err != nil {
return err
}
latestHash, err := DBHash(ctx, tx, "main")
latestHash, err := DBDigest(ctx, tx, "main")
if err != nil {
return err
}
if latestHash == curHash {
s.logf("Schema is up-to-date at digest %s", curHash)
return nil
}

hr, err := History(ctx, tx)
if err != nil {
return fmt.Errorf("reading update history: %w", err)
} else if len(hr) == 0 {
// Case 1: There is no schema present in the history table.
if err := checkSchema(ctx, tx, "main", s.Current); err == nil {
s.logf("No schema is defined, applying initial schema %s", curHash)
if latestHash != curHash {
if !schemaIsEmpty(ctx, tx, "main") {
return fmt.Errorf("unmanaged schema already present (%w)", err)
}
if _, err := tx.ExecContext(ctx, s.Current); err != nil {
return fmt.Errorf("apply current schema: %w", err)
return fmt.Errorf("apply schema: %w", err)
}
} else if errors.Is(err, errSchemaExists) {
s.logf("Schema %s is already current; updating history", curHash)
// fall through
} else {
return fmt.Errorf("unmanaged schema already present (%w)", err)
}
s.logf("Schema %s is already current; updating history", curHash)
if err := s.addVersion(ctx, tx, HistoryRow{
Timestamp: time.Now().UnixMicro(),
Digest: curHash,
Expand All @@ -194,7 +188,13 @@ func (s *Schema) Apply(ctx context.Context, db *sql.DB) error {
return tx.Commit()
}

// Case 2: The current schema is not the latest. Apply pending changes.
// Case 2: The current schema is up-to-date.
if latestHash == curHash {
s.logf("Schema is up-to-date at digest %s", curHash)
return nil
}

// Case 3: The current schema is not the latest. Apply pending changes.
last := hr[len(hr)-1]
s.Logf("Last update at %s (%s)", formatTime(last.Timestamp), last.Digest)
s.logf("Latest DB schema is %s", latestHash)
Expand All @@ -216,7 +216,7 @@ func (s *Schema) Apply(ctx context.Context, db *sql.DB) error {
if err := update.Apply(uctx, tx); err != nil {
return fmt.Errorf("update failed at digest %s: %w", update.Source, err)
}
conf, err := DBHash(uctx, tx, "main")
conf, err := DBDigest(uctx, tx, "main")
if err != nil {
return fmt.Errorf("confirming update: %w", err)
}
Expand Down Expand Up @@ -272,7 +272,7 @@ func (s *Schema) Check() error {
if s.Current == "" {
return errors.New("no current schema is defined")
}
hc, err := SchemaHash(s.Current)
hc, err := SQLDigest(s.Current)
if err != nil {
return err
}
Expand Down Expand Up @@ -330,9 +330,9 @@ type HistoryRow struct {
Schema string // The SQL of the schema at this update
}

// SchemaHash computes a hex-encoded SHA256 digest of the SQLite schema encoded
// SQLDigest computes a hex-encoded SHA256 digest of the SQLite schema encoded
// by the specified string.
func SchemaHash(text string) (string, error) {
func SQLDigest(text string) (string, error) {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
return "", fmt.Errorf("open hash db: %w", err)
Expand All @@ -347,9 +347,9 @@ func SchemaHash(text string) (string, error) {
return hex.EncodeToString(h.Sum(nil)), nil
}

// DBHash computes a hex-encoded SHA256 digest of the SQLite schema encoded in
// DBDigest computes a hex-encoded SHA256 digest of the SQLite schema encoded in
// the specified database.
func DBHash(ctx context.Context, db DBConn, root string) (string, error) {
func DBDigest(ctx context.Context, db DBConn, root string) (string, error) {
sr, err := readSchema(ctx, db, root)
if err != nil {
return "", err
Expand Down
2 changes: 1 addition & 1 deletion squibble_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func checkTableSchema(t *testing.T, db *sql.DB, table, want string) {

func mustHash(t *testing.T, text string) string {
t.Helper()
h, err := squibble.SchemaHash(text)
h, err := squibble.SQLDigest(text)
if err != nil {
t.Fatalf("SchemaHash failed: %v", err)
}
Expand Down
27 changes: 6 additions & 21 deletions validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"cmp"
"context"
"database/sql"
"errors"
"fmt"
"slices"

Expand Down Expand Up @@ -107,27 +106,13 @@ func readSchema(ctx context.Context, db DBConn, root string) ([]schemaRow, error
return out, nil
}

var errSchemaExists = errors.New("schema is already applied")

// checkSchema reports whether the schema for the root database is compatible
// with the given schema text. It returns nil if the root schema is essentially
// empty (possibly but for a history table); it returns errSchemaExists if the
// root schema exists and is equivalent. Any other error means the schemata are
// either incompatible, or unreadable.
func checkSchema(ctx context.Context, db DBConn, root, schema string) error {
// schemaIsEmpty reports whether the schema for the specified database is
// essentially empty (meaning, it is either empty or contains only a history
// table).
func schemaIsEmpty(ctx context.Context, db DBConn, root string) bool {
main, err := readSchema(ctx, db, root)
if err != nil {
return err
} else if len(main) == 0 || main[0].Name == historyTableName {
return nil
}

comp, err := schemaTextToRows(ctx, db, schema)
if err != nil {
return err
}
if diff := gocmp.Diff(main, comp); diff != "" {
return ValidationError{Diff: diff}
return false
}
return errSchemaExists
return len(main) == 0 || (len(main) == 1 && main[0].Name == historyTableName)
}

0 comments on commit 526ba6f

Please sign in to comment.