Skip to content

Commit

Permalink
[MySQL] Check that the DB schema is compatible (#458)
Browse files Browse the repository at this point in the history
The version is written at schema creation, and checked on startup. If they are different, it fails. Towards #450.
  • Loading branch information
mhutchinson authored Jan 23, 2025
1 parent cd46900 commit 754eacd
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 0 deletions.
4 changes: 4 additions & 0 deletions storage/mysql/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ The DB layout has been designed such that serving any read request is a point lo

### Table Schema

#### `Tessera`

A single row that records the current version of the Tessera schema and data compatibility.

#### `Checkpoint`

A single row that records the current published checkpoint.
Expand Down
21 changes: 21 additions & 0 deletions storage/mysql/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
)

const (
selectCompatibilityVersionSQL = "SELECT `compatibilityVersion` FROM `Tessera` WHERE `id` = 0"
selectCheckpointByIDSQL = "SELECT `note`, `published_at` FROM `Checkpoint` WHERE `id` = ?"
selectCheckpointByIDForUpdateSQL = selectCheckpointByIDSQL + " FOR UPDATE"
replaceCheckpointSQL = "REPLACE INTO `Checkpoint` (`id`, `note`, `published_at`) VALUES (?, ?, ?)"
Expand All @@ -52,6 +53,8 @@ const (
checkpointID = 0
treeStateID = 0

schemaCompatibilityVersion = 1

minCheckpointInterval = time.Second
)

Expand Down Expand Up @@ -85,6 +88,9 @@ func New(ctx context.Context, db *sql.DB, opts ...func(*options.StorageOptions))
if s.newCheckpoint == nil {
return nil, errors.New("tessera.WithCheckpointSigner must be provided in New()")
}
if err := s.ensureVersion(ctx, schemaCompatibilityVersion); err != nil {
return nil, fmt.Errorf("incompatible schema version: %v", err)
}

s.queue = storage.NewQueue(ctx, opt.BatchMaxAge, opt.BatchMaxSize, s.sequenceBatch)

Expand All @@ -110,6 +116,21 @@ func New(ctx context.Context, db *sql.DB, opts ...func(*options.StorageOptions))
return s, nil
}

func (s *Storage) ensureVersion(ctx context.Context, wantVersion uint8) error {
row := s.db.QueryRowContext(ctx, selectCompatibilityVersionSQL)
if row.Err() != nil {
return row.Err()
}
var gotVersion uint8
if err := row.Scan(&gotVersion); err != nil {
return fmt.Errorf("failed to read Tessera version from DB: %v", err)
}
if gotVersion != wantVersion {
return fmt.Errorf("DB has Tessera compatibility version of %d, but version %d required", gotVersion, wantVersion)
}
return nil
}

// maybeInitTree will insert an initial "empty tree" row into the
// TreeState table iff no row already exists.
//
Expand Down
13 changes: 13 additions & 0 deletions storage/mysql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@

-- MySQL version of the Trillian Tessera database schema.

-- "Tessera" table stores a single row that is the version of this schema
-- and the data formats within it. This is read at startup to prevent Tessera
-- running against a database with an incompatible format.
CREATE TABLE IF NOT EXISTS Tessera (
-- id is expected to be always 0 to maintain a maximum of a single row.
`id` TINYINT UNSIGNED NOT NULL,
-- compatibilityVersion is the version of this schema and the data within it.
`compatibilityVersion` BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
);

INSERT IGNORE INTO Tessera (`id`, `compatibilityVersion`) VALUES (0, 1);

-- "Checkpoint" table stores a single row that records the latest _published_ checkpoint for the log.
-- This is stored separately from the TreeState in order to enable publishing of commitments to updated tree states to happen
-- on an indepentent timeframe to the internal updating of state.
Expand Down

0 comments on commit 754eacd

Please sign in to comment.