@@ -4,16 +4,17 @@ import (
44 "context"
55 stderrors "errors"
66 "fmt"
7+ "maps"
8+ "os"
9+ "path"
10+ "slices"
11+
712 "github.com/icinga/icinga-go-library/backoff"
813 "github.com/icinga/icinga-go-library/database"
914 "github.com/icinga/icinga-go-library/retry"
1015 "github.com/icinga/icingadb/internal"
1116 "github.com/jmoiron/sqlx"
1217 "github.com/pkg/errors"
13- "maps"
14- "os"
15- "path"
16- "slices"
1718)
1819
1920// ErrSchemaNotExists implies that no Icinga DB schema has been imported.
@@ -23,11 +24,16 @@ var ErrSchemaNotExists = stderrors.New("no database schema exists")
2324// missed the schema upgrade.
2425var ErrSchemaMismatch = stderrors .New ("unexpected database schema version" )
2526
27+ // ErrSchemaImperfect implies some non critical failure condition of the database schema.
28+ var ErrSchemaImperfect = stderrors .New ("imperfect database schema" )
29+
2630// CheckSchema verifies the correct database schema is present.
2731//
2832// This function returns the following error types, possibly wrapped:
2933// - If no schema exists, the error returned is ErrSchemaNotExists.
3034// - If the schema version does not match the expected version, the error returned is ErrSchemaMismatch.
35+ // - If there are non fatal database schema conditions, ErrSchemaImperfect is returned. This error must
36+ // be reported back to the user, but should not lead in a program termination.
3137// - Otherwise, the original error is returned, for example in case of general database problems.
3238func CheckSchema (ctx context.Context , db * database.DB ) error {
3339 var schemaVersions map [uint16 ]string
@@ -53,7 +59,7 @@ func CheckSchema(ctx context.Context, db *database.DB) error {
5359 err := retry .WithBackoff (
5460 ctx ,
5561 func (ctx context.Context ) error {
56- query := "SELECT version FROM icingadb_schema ORDER BY version ASC"
62+ query := "SELECT version FROM icingadb_schema ORDER BY id ASC"
5763 if err := db .SelectContext (ctx , & versions , query ); err != nil {
5864 return database .CantPerformQuery (err , query )
5965 }
@@ -66,38 +72,51 @@ func CheckSchema(ctx context.Context, db *database.DB) error {
6672 return errors .Wrap (err , "can't check database schema version" )
6773 }
6874
75+ // In the following, multiple error conditions are checked.
76+ //
77+ // Since their error messages are trivial and mostly caused by users, we don't need
78+ // to print a stack trace here. However, since errors.Errorf() does this automatically,
79+ // we need to use fmt.Errorf() instead.
80+
81+ // Check if any schema was imported.
6982 if len (versions ) == 0 {
7083 return fmt .Errorf ("%w: no database schema version is stored in the database" , ErrSchemaMismatch )
7184 }
7285
73- // Check if each schema update between the initial import and the latest version was applied or, in other words,
74- // that no schema update was left out. The loop goes over the ascending sorted array of schema versions, verifying
75- // that each element's successor is the increment of this version, ensuring no gaps in between.
76- for i := 0 ; i < len (versions )- 1 ; i ++ {
77- if versions [i ] != versions [i + 1 ]- 1 {
78- missing := versions [i ] + 1
86+ // Check if the latest schema version was imported.
87+ if latestVersion := slices .Max (versions ); latestVersion != expectedDbSchemaVersion {
88+ return fmt .Errorf ("%w: v%d (expected v%d), " +
89+ "please apply the %s.sql schema upgrade file to your database after upgrading Icinga DB: " +
90+ "https://icinga.com/docs/icinga-db/latest/doc/04-Upgrading/" ,
91+ ErrSchemaMismatch , latestVersion , expectedDbSchemaVersion , schemaVersions [expectedDbSchemaVersion ])
92+ }
7993
94+ // Check if all schema updates between the oldest schema version and the expected version were applied.
95+ for version := slices .Min (versions ); version < expectedDbSchemaVersion ; version ++ {
96+ if ! slices .Contains (versions , version ) {
8097 release := "UNKNOWN"
81- if releaseVersion , ok := schemaVersions [missing ]; ok {
98+ if releaseVersion , ok := schemaVersions [version ]; ok {
8299 release = releaseVersion
83100 }
84101
85102 return fmt .Errorf (
86103 "%w: incomplete database schema upgrade: intermediate version v%d (%s) is missing, " +
87104 "please inspect the icingadb_schema database table and ensure that all database " +
88105 "migrations were applied in order after upgrading Icinga DB" ,
89- ErrSchemaMismatch , missing , release )
106+ ErrSchemaMismatch , version , release )
90107 }
91108 }
92109
93- if latestVersion := versions [len (versions )- 1 ]; latestVersion != expectedDbSchemaVersion {
94- // Since these error messages are trivial and mostly caused by users, we don't need
95- // to print a stack trace here. However, since errors.Errorf() does this automatically,
96- // we need to use fmt instead.
97- return fmt .Errorf ("%w: v%d (expected v%d), " +
98- "please apply the %s.sql schema upgrade file to your database after upgrading Icinga DB: " +
99- "https://icinga.com/docs/icinga-db/latest/doc/04-Upgrading/" ,
100- ErrSchemaMismatch , latestVersion , expectedDbSchemaVersion , schemaVersions [expectedDbSchemaVersion ])
110+ // Extend the prior check by checking if the schema updates were applied in a monotonic increasing order.
111+ // However, this returns an ErrSchemaImperfect error instead of an ErrSchemaMismatch.
112+ for i := 0 ; i < len (versions )- 1 ; i ++ {
113+ if versions [i ] != versions [i + 1 ]- 1 {
114+ return fmt .Errorf (
115+ "%w: unexpected schema upgrade order after schema version %d, " +
116+ "please inspect the icingadb_schema database table and ensure that all database " +
117+ "migrations were applied in order after upgrading Icinga DB" ,
118+ ErrSchemaImperfect , versions [i ])
119+ }
101120 }
102121
103122 return nil
0 commit comments