Skip to content

Commit a65cf78

Browse files
committed
v4 for db migrations
1 parent 291b989 commit a65cf78

14 files changed

Lines changed: 1425 additions & 24 deletions

File tree

config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"github.com/spf13/viper"
1111
)
1212

13-
const VERSION = "3.14"
13+
const VERSION = "4.0"
1414

1515
type Config struct {
1616
Server ServerConfig

internal/app/app.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ func (a *App) InitDB() error {
248248
}
249249

250250
// Initialize database schema if needed
251-
if err := database.InitializeDatabase(db, a.config.RootEmail); err != nil {
251+
if err := database.InitializeDatabase(db, a.config.RootEmail, a.config, a.logger); err != nil {
252252
db.Close()
253253
return fmt.Errorf("failed to initialize database schema: %w", err)
254254
}

internal/database/init.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
package database
22

33
import (
4+
"context"
45
"database/sql"
56
"fmt"
67
"time"
78

89
"github.com/google/uuid"
910

11+
"github.com/Notifuse/notifuse/config"
1012
"github.com/Notifuse/notifuse/internal/database/schema"
1113
"github.com/Notifuse/notifuse/internal/domain"
14+
"github.com/Notifuse/notifuse/internal/migrations"
15+
"github.com/Notifuse/notifuse/pkg/logger"
1216
)
1317

1418
// InitializeDatabase creates all necessary database tables if they don't exist
15-
func InitializeDatabase(db *sql.DB, rootEmail string) error {
19+
func InitializeDatabase(db *sql.DB, rootEmail string, cfg *config.Config, migrationLogger logger.Logger) error {
1620
// Run all table creation queries
1721
for _, query := range schema.TableDefinitions {
1822
if _, err := db.Exec(query); err != nil {
@@ -27,6 +31,14 @@ func InitializeDatabase(db *sql.DB, rootEmail string) error {
2731
}
2832
}
2933

34+
// Run version migrations
35+
migrationManager := migrations.NewManager(migrationLogger)
36+
37+
ctx := context.Background()
38+
if err := migrationManager.RunMigrations(ctx, cfg, db); err != nil {
39+
return fmt.Errorf("failed to run migrations: %w", err)
40+
}
41+
3042
// Create root user if it doesn't exist
3143
if rootEmail != "" {
3244
// Check if root user exists

internal/database/init_test.go

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,22 @@ import (
44
"database/sql"
55
"testing"
66

7+
"github.com/Notifuse/notifuse/config"
78
"github.com/Notifuse/notifuse/internal/database/schema"
9+
"github.com/Notifuse/notifuse/pkg/logger"
810

911
"github.com/DATA-DOG/go-sqlmock"
1012
"github.com/stretchr/testify/assert"
1113
"github.com/stretchr/testify/require"
1214
)
1315

1416
func TestInitializeDatabase(t *testing.T) {
17+
testConfig := &config.Config{
18+
Version: "3.14",
19+
LogLevel: "info",
20+
}
21+
testLogger := logger.NewLoggerWithLevel("info")
22+
1523
t.Run("creates tables successfully", func(t *testing.T) {
1624
db, mock, err := sqlmock.New()
1725
require.NoError(t, err)
@@ -23,11 +31,20 @@ func TestInitializeDatabase(t *testing.T) {
2331
}
2432

2533
// Setup expectations for migration statements
26-
for range schema.MigrationStatements {
34+
for range schema.GetMigrationStatements() {
2735
mock.ExpectExec("").WillReturnResult(sqlmock.NewResult(0, 0))
2836
}
2937

30-
err = InitializeDatabase(db, "")
38+
// Setup expectations for migration manager
39+
// Migration manager checks for existing version (first run)
40+
mock.ExpectQuery(`SELECT value FROM settings WHERE key = 'db_version'`).
41+
WillReturnError(sql.ErrNoRows) // No version exists yet
42+
43+
// Migration manager initializes version for first run
44+
mock.ExpectExec(`INSERT INTO settings`).
45+
WillReturnResult(sqlmock.NewResult(1, 1))
46+
47+
err = InitializeDatabase(db, "", testConfig, testLogger)
3148
assert.NoError(t, err)
3249
assert.NoError(t, mock.ExpectationsWereMet())
3350
})
@@ -43,10 +60,16 @@ func TestInitializeDatabase(t *testing.T) {
4360
}
4461

4562
// Setup expectations for migration statements
46-
for range schema.MigrationStatements {
63+
for range schema.GetMigrationStatements() {
4764
mock.ExpectExec("").WillReturnResult(sqlmock.NewResult(0, 0))
4865
}
4966

67+
// Setup expectations for migration manager
68+
mock.ExpectQuery(`SELECT value FROM settings WHERE key = 'db_version'`).
69+
WillReturnError(sql.ErrNoRows)
70+
mock.ExpectExec(`INSERT INTO settings`).
71+
WillReturnResult(sqlmock.NewResult(1, 1))
72+
5073
// Root user doesn't exist
5174
mock.ExpectQuery("SELECT EXISTS").
5275
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(false))
@@ -55,7 +78,7 @@ func TestInitializeDatabase(t *testing.T) {
5578
mock.ExpectExec("INSERT INTO users").
5679
WillReturnResult(sqlmock.NewResult(1, 1))
5780

58-
err = InitializeDatabase(db, "admin@example.com")
81+
err = InitializeDatabase(db, "admin@example.com", testConfig, testLogger)
5982
assert.NoError(t, err)
6083
assert.NoError(t, mock.ExpectationsWereMet())
6184
})
@@ -71,17 +94,23 @@ func TestInitializeDatabase(t *testing.T) {
7194
}
7295

7396
// Setup expectations for migration statements
74-
for range schema.MigrationStatements {
97+
for range schema.GetMigrationStatements() {
7598
mock.ExpectExec("").WillReturnResult(sqlmock.NewResult(0, 0))
7699
}
77100

101+
// Setup expectations for migration manager
102+
mock.ExpectQuery(`SELECT value FROM settings WHERE key = 'db_version'`).
103+
WillReturnError(sql.ErrNoRows)
104+
mock.ExpectExec(`INSERT INTO settings`).
105+
WillReturnResult(sqlmock.NewResult(1, 1))
106+
78107
// Root user already exists
79108
mock.ExpectQuery("SELECT EXISTS").
80109
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
81110

82111
// No insert should be made
83112

84-
err = InitializeDatabase(db, "admin@example.com")
113+
err = InitializeDatabase(db, "admin@example.com", testConfig, testLogger)
85114
assert.NoError(t, err)
86115
assert.NoError(t, mock.ExpectationsWereMet())
87116
})
@@ -94,7 +123,7 @@ func TestInitializeDatabase(t *testing.T) {
94123
// First table creation fails
95124
mock.ExpectExec("").WillReturnError(sql.ErrConnDone)
96125

97-
err = InitializeDatabase(db, "admin@example.com")
126+
err = InitializeDatabase(db, "admin@example.com", testConfig, testLogger)
98127
assert.Error(t, err)
99128
assert.NoError(t, mock.ExpectationsWereMet())
100129
})
@@ -110,15 +139,21 @@ func TestInitializeDatabase(t *testing.T) {
110139
}
111140

112141
// Setup expectations for migration statements
113-
for range schema.MigrationStatements {
142+
for range schema.GetMigrationStatements() {
114143
mock.ExpectExec("").WillReturnResult(sqlmock.NewResult(0, 0))
115144
}
116145

146+
// Setup expectations for migration manager
147+
mock.ExpectQuery(`SELECT value FROM settings WHERE key = 'db_version'`).
148+
WillReturnError(sql.ErrNoRows)
149+
mock.ExpectExec(`INSERT INTO settings`).
150+
WillReturnResult(sqlmock.NewResult(1, 1))
151+
117152
// Query fails
118153
mock.ExpectQuery("SELECT EXISTS").
119154
WillReturnError(sql.ErrConnDone)
120155

121-
err = InitializeDatabase(db, "admin@example.com")
156+
err = InitializeDatabase(db, "admin@example.com", testConfig, testLogger)
122157
assert.Error(t, err)
123158
assert.NoError(t, mock.ExpectationsWereMet())
124159
})
@@ -134,10 +169,16 @@ func TestInitializeDatabase(t *testing.T) {
134169
}
135170

136171
// Setup expectations for migration statements
137-
for range schema.MigrationStatements {
172+
for range schema.GetMigrationStatements() {
138173
mock.ExpectExec("").WillReturnResult(sqlmock.NewResult(0, 0))
139174
}
140175

176+
// Setup expectations for migration manager
177+
mock.ExpectQuery(`SELECT value FROM settings WHERE key = 'db_version'`).
178+
WillReturnError(sql.ErrNoRows)
179+
mock.ExpectExec(`INSERT INTO settings`).
180+
WillReturnResult(sqlmock.NewResult(1, 1))
181+
141182
// Root user doesn't exist
142183
mock.ExpectQuery("SELECT EXISTS").
143184
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(false))
@@ -146,7 +187,7 @@ func TestInitializeDatabase(t *testing.T) {
146187
mock.ExpectExec("INSERT INTO users").
147188
WillReturnError(sql.ErrConnDone)
148189

149-
err = InitializeDatabase(db, "admin@example.com")
190+
err = InitializeDatabase(db, "admin@example.com", testConfig, testLogger)
150191
assert.Error(t, err)
151192
assert.NoError(t, mock.ExpectationsWereMet())
152193
})

internal/database/schema/system_tables.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
package schema
22

3-
import (
4-
"fmt"
5-
6-
"github.com/Notifuse/notifuse/config"
7-
)
3+
// Schema definitions - no external imports needed
84

95
// TableDefinitions contains all the SQL statements to create the database tables
106
// Don't put REFERENCES and don't put CHECK constraints in the CREATE TABLE statements
@@ -37,6 +33,7 @@ var TableDefinitions = []string{
3733
user_id UUID NOT NULL,
3834
workspace_id VARCHAR(20) NOT NULL,
3935
role VARCHAR(20) NOT NULL,
36+
permissions JSONB DEFAULT '{}'::jsonb,
4037
created_at TIMESTAMP NOT NULL,
4138
updated_at TIMESTAMP NOT NULL,
4239
PRIMARY KEY (user_id, workspace_id)
@@ -114,11 +111,9 @@ var MigrationStatements = []string{
114111
)`,
115112
}
116113

117-
// GetMigrationStatements returns migration statements with the current version
114+
// GetMigrationStatements returns migration statements for database schema setup
118115
func GetMigrationStatements() []string {
119-
versionInsert := fmt.Sprintf(`INSERT INTO settings (key, value) VALUES ('db_version', '%s') ON CONFLICT (key) DO UPDATE SET value = '%s', updated_at = CURRENT_TIMESTAMP`, config.VERSION, config.VERSION)
120-
121-
return append(MigrationStatements, versionInsert)
116+
return MigrationStatements
122117
}
123118

124119
// TableNames returns a list of all table names in creation order
@@ -130,4 +125,5 @@ var TableNames = []string{
130125
"workspace_invitations",
131126
"broadcasts",
132127
"tasks",
128+
"settings",
133129
}

internal/migrations/interfaces.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package migrations
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
7+
"github.com/Notifuse/notifuse/config"
8+
"github.com/Notifuse/notifuse/internal/domain"
9+
)
10+
11+
// DBExecutor represents a database connection that can execute queries
12+
type DBExecutor interface {
13+
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
14+
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
15+
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
16+
}
17+
18+
// MajorMigrationInterface defines a major version migration
19+
type MajorMigrationInterface interface {
20+
GetMajorVersion() float64
21+
HasSystemUpdate() bool
22+
HasWorkspaceUpdate() bool
23+
UpdateSystem(ctx context.Context, config *config.Config, db DBExecutor) error
24+
UpdateWorkspace(ctx context.Context, config *config.Config, workspace *domain.Workspace, db DBExecutor) error
25+
}
26+
27+
// MigrationManager interface for managing migrations
28+
type MigrationManager interface {
29+
GetCurrentDBVersion(ctx context.Context, db *sql.DB) (float64, error, bool)
30+
SetCurrentDBVersion(ctx context.Context, db *sql.DB, version float64) error
31+
RunMigrations(ctx context.Context, config *config.Config, db *sql.DB) error
32+
}
33+
34+
// MigrationRegistry manages registered migrations
35+
type MigrationRegistry interface {
36+
Register(migration MajorMigrationInterface)
37+
GetMigrations() []MajorMigrationInterface
38+
GetMigration(version float64) (MajorMigrationInterface, bool)
39+
}

0 commit comments

Comments
 (0)