@@ -2,11 +2,15 @@ package icingadb
2
2
3
3
import (
4
4
"context"
5
+ stderrors "errors"
5
6
"fmt"
6
7
"github.com/icinga/icinga-go-library/backoff"
7
8
"github.com/icinga/icinga-go-library/database"
8
9
"github.com/icinga/icinga-go-library/retry"
10
+ "github.com/jmoiron/sqlx"
9
11
"github.com/pkg/errors"
12
+ "os"
13
+ "path"
10
14
"time"
11
15
)
12
16
@@ -15,27 +19,45 @@ const (
15
19
expectedPostgresSchemaVersion = 4
16
20
)
17
21
18
- // CheckSchema asserts the database schema of the expected version being present.
22
+ // ErrSchemaNotExists implies that no Icinga DB schema has been imported.
23
+ var ErrSchemaNotExists = stderrors .New ("no database schema exists" )
24
+
25
+ // ErrSchemaMismatch implies an unexpected schema version, most likely after Icinga DB was updated but the database
26
+ // missed the schema upgrade.
27
+ var ErrSchemaMismatch = stderrors .New ("unexpected database schema version" )
28
+
29
+ // CheckSchema verifies the correct database schema is present.
30
+ //
31
+ // This function returns the following error types, possibly wrapped:
32
+ // - If no schema exists, the error returned is ErrSchemaNotExists.
33
+ // - If the schema version does not match the expected version, the error returned is ErrSchemaMismatch.
34
+ // - Otherwise, the original error is returned, for example in case of general database problems.
19
35
func CheckSchema (ctx context.Context , db * database.DB ) error {
20
36
var expectedDbSchemaVersion uint16
21
37
switch db .DriverName () {
22
38
case database .MySQL :
23
39
expectedDbSchemaVersion = expectedMysqlSchemaVersion
24
40
case database .PostgreSQL :
25
41
expectedDbSchemaVersion = expectedPostgresSchemaVersion
42
+ default :
43
+ return errors .Errorf ("unsupported database driver %q" , db .DriverName ())
26
44
}
27
45
28
- var version uint16
46
+ if hasSchemaTable , err := db .HasTable (ctx , "icingadb_schema" ); err != nil {
47
+ return errors .Wrap (err , "can't verify existence of database schema table" )
48
+ } else if ! hasSchemaTable {
49
+ return ErrSchemaNotExists
50
+ }
29
51
52
+ var version uint16
30
53
err := retry .WithBackoff (
31
54
ctx ,
32
- func (ctx context.Context ) ( err error ) {
55
+ func (ctx context.Context ) error {
33
56
query := "SELECT version FROM icingadb_schema ORDER BY id DESC LIMIT 1"
34
- err = db .QueryRowxContext (ctx , query ).Scan (& version )
35
- if err != nil {
36
- err = database .CantPerformQuery (err , query )
57
+ if err := db .QueryRowxContext (ctx , query ).Scan (& version ); err != nil {
58
+ return database .CantPerformQuery (err , query )
37
59
}
38
- return
60
+ return nil
39
61
},
40
62
retry .Retryable ,
41
63
backoff .NewExponentialWithJitter (128 * time .Millisecond , 1 * time .Minute ),
@@ -48,11 +70,50 @@ func CheckSchema(ctx context.Context, db *database.DB) error {
48
70
// Since these error messages are trivial and mostly caused by users, we don't need
49
71
// to print a stack trace here. However, since errors.Errorf() does this automatically,
50
72
// we need to use fmt instead.
51
- return fmt .Errorf (
52
- "unexpected database schema version: v%d (expected v%d), please make sure you have applied all database" +
53
- " migrations after upgrading Icinga DB" , version , expectedDbSchemaVersion ,
73
+ return fmt .Errorf ("%w: v%d (expected v%d), please make sure you have applied all database" +
74
+ " migrations after upgrading Icinga DB" , ErrSchemaMismatch , version , expectedDbSchemaVersion ,
54
75
)
55
76
}
56
77
57
78
return nil
58
79
}
80
+
81
+ // ImportSchema performs an initial schema import in the db.
82
+ //
83
+ // This function assumes that no schema exists. So it should only be called after a prior CheckSchema call.
84
+ func ImportSchema (
85
+ ctx context.Context ,
86
+ db * database.DB ,
87
+ databaseSchemaDir string ,
88
+ ) error {
89
+ var schemaFileDirPart string
90
+ switch db .DriverName () {
91
+ case database .MySQL :
92
+ schemaFileDirPart = "mysql"
93
+ case database .PostgreSQL :
94
+ schemaFileDirPart = "pgsql"
95
+ default :
96
+ return errors .Errorf ("unsupported database driver %q" , db .DriverName ())
97
+ }
98
+
99
+ schemaFile := path .Join (databaseSchemaDir , schemaFileDirPart , "schema.sql" )
100
+ schema , err := os .ReadFile (schemaFile ) // #nosec G304 -- path is constructed from "trusted" command line user input
101
+ if err != nil {
102
+ return errors .Wrapf (err , "can't open schema file %q" , schemaFile )
103
+ }
104
+
105
+ queries := []string {string (schema )}
106
+ if db .DriverName () == database .MySQL {
107
+ // MySQL/MariaDB requires the schema to be imported on a statement by statement basis.
108
+ queries = database .MysqlSplitStatements (string (schema ))
109
+ }
110
+
111
+ return errors .Wrapf (db .ExecTx (ctx , func (ctx context.Context , tx * sqlx.Tx ) error {
112
+ for _ , query := range queries {
113
+ if _ , err := tx .ExecContext (ctx , query ); err != nil {
114
+ return errors .Wrap (database .CantPerformQuery (err , query ), "can't perform schema import" )
115
+ }
116
+ }
117
+ return nil
118
+ }), "can't import database schema from %q" , schemaFile )
119
+ }
0 commit comments