@@ -2,6 +2,8 @@ package provisioner
2
2
3
3
import (
4
4
"context"
5
+ "database/sql"
6
+ "errors"
5
7
"fmt"
6
8
"time"
7
9
@@ -38,6 +40,15 @@ func provisionMysql(mysqlPort int, recreate bool) InMemResourceProvisionerFn {
38
40
dbName := strcase .ToLowerSnake (deployment .Payload .Module ) + "_" + strcase .ToLowerSnake (res .ResourceID ())
39
41
40
42
logger .Infof ("Provisioning mysql database: %s" , dbName ) //nolint
43
+ db , ok := res .(* schema.Database )
44
+ if ! ok {
45
+ return nil , fmt .Errorf ("expected database, got %T" , res )
46
+ }
47
+ migrationHash := ""
48
+ for migration := range slices.FilterVariants [* schema.MetadataSQLMigration ](db .Metadata ) {
49
+ migrationHash = migration .Digest
50
+ break
51
+ }
41
52
42
53
// We assume that the DB hsas already been started when running in dev mode
43
54
mysqlDSN , err := dev .SetupMySQL (ctx , mysqlPort )
@@ -52,7 +63,7 @@ func provisionMysql(mysqlPort int, recreate bool) InMemResourceProvisionerFn {
52
63
case <- timeout :
53
64
return nil , fmt .Errorf ("failed to query database: %w" , err )
54
65
case <- retry .C :
55
- event , err := establishMySQLDB (ctx , mysqlDSN , dbName , mysqlPort , recreate )
66
+ event , err := establishMySQLDB (ctx , mysqlDSN , dbName , mysqlPort , recreate , migrationHash )
56
67
if err != nil {
57
68
logger .Debugf ("failed to establish mysql database: %s" , err .Error ())
58
69
continue
@@ -68,8 +79,9 @@ func provisionMysql(mysqlPort int, recreate bool) InMemResourceProvisionerFn {
68
79
}
69
80
}
70
81
71
- func establishMySQLDB (ctx context.Context , mysqlDSN string , dbName string , mysqlPort int , recreate bool ) (* schema.DatabaseRuntimeConnections , error ) {
82
+ func establishMySQLDB (ctx context.Context , mysqlDSN string , dbName string , mysqlPort int , recreate bool , migrationHash string ) (* schema.DatabaseRuntimeConnections , error ) {
72
83
conn , err := otelsql .Open ("mysql" , mysqlDSN )
84
+ logger := log .FromContext (ctx )
73
85
if err != nil {
74
86
return nil , fmt .Errorf ("failed to connect to mysql: %w" , err )
75
87
}
@@ -82,6 +94,31 @@ func establishMySQLDB(ctx context.Context, mysqlDSN string, dbName string, mysql
82
94
defer res .Close ()
83
95
84
96
exists := res .Next ()
97
+
98
+ if migrationHash != "" {
99
+ _ , err := conn .Exec ("CREATE TABLE IF NOT EXISTS migrations (db VARCHAR(255) PRIMARY KEY NOT NULL, migration VARCHAR(255) NOT NULL)" )
100
+ if err != nil {
101
+ return nil , fmt .Errorf ("failed to create migrations tracking table: %w" , err )
102
+ }
103
+ if exists && ! recreate {
104
+ // We might still need to recreate the database if the schema has changed
105
+ existing := ""
106
+ err := conn .QueryRow ("SELECT migration FROM migrations WHERE db=?" , dbName ).Scan (& existing )
107
+ if err != nil {
108
+ if ! errors .Is (err , sql .ErrNoRows ) {
109
+ return nil , fmt .Errorf ("failed to query migrations table: %w" , err )
110
+ }
111
+ logger .Debugf ("No existing migration found" )
112
+ } else {
113
+ logger .Debugf ("existing migration: %s , current migration %s" , existing , migrationHash )
114
+ if existing != migrationHash {
115
+ logger .Infof ("Recreating database %q due to schema change" , dbName ) //nolint
116
+ recreate = true
117
+ }
118
+ }
119
+ }
120
+ }
121
+
85
122
if exists && recreate {
86
123
_ , err = conn .ExecContext (ctx , "DROP DATABASE " + dbName )
87
124
if err != nil {
@@ -97,6 +134,13 @@ func establishMySQLDB(ctx context.Context, mysqlDSN string, dbName string, mysql
97
134
98
135
dsn := dsn .MySQLDSN (dbName , dsn .Port (mysqlPort ))
99
136
137
+ if migrationHash != "" {
138
+ _ , err := conn .Exec ("INSERT INTO migrations (db, migration) VALUES (?, ?) ON DUPLICATE KEY UPDATE migration = ?" , dbName , migrationHash , migrationHash )
139
+ if err != nil {
140
+ return nil , fmt .Errorf ("failed to insert migration hash: %w" , err )
141
+ }
142
+ }
143
+
100
144
return & schema.DatabaseRuntimeConnections {
101
145
Write : & schema.DSNDatabaseConnector {DSN : dsn , Database : dbName },
102
146
Read : & schema.DSNDatabaseConnector {DSN : dsn , Database : dbName },
@@ -123,9 +167,14 @@ func ProvisionMySQLForTest(ctx context.Context, moduleName string, id string) (s
123
167
124
168
}
125
169
126
- func provisionPostgres (postgresPort int , recreate bool ) InMemResourceProvisionerFn {
170
+ func provisionPostgres (postgresPort int , alwaysRecreate bool ) InMemResourceProvisionerFn {
127
171
return func (ctx context.Context , changeset key.Changeset , deployment key.Deployment , resource schema.Provisioned ) (* schema.RuntimeElement , error ) {
172
+ recreate := alwaysRecreate
128
173
logger := log .FromContext (ctx ).Deployment (deployment )
174
+ db , ok := resource .(* schema.Database )
175
+ if ! ok {
176
+ return nil , fmt .Errorf ("expected database, got %T" , resource )
177
+ }
129
178
130
179
dbName := strcase .ToLowerSnake (deployment .Payload .Module ) + "_" + strcase .ToLowerSnake (resource .ResourceID ())
131
180
logger .Infof ("Provisioning postgres database: %s" , dbName ) //nolint
@@ -150,6 +199,30 @@ func provisionPostgres(postgresPort int, recreate bool) InMemResourceProvisioner
150
199
defer res .Close ()
151
200
152
201
exists := res .Next ()
202
+ migrationHash := ""
203
+ for migration := range slices.FilterVariants [* schema.MetadataSQLMigration ](db .Metadata ) {
204
+ _ , err := conn .Exec ("CREATE TABLE IF NOT EXISTS migrations (db VARCHAR PRIMARY KEY NOT NULL, migration VARCHAR NOT NULL)" )
205
+ if err != nil {
206
+ return nil , fmt .Errorf ("failed to create migrations tracking table: %w" , err )
207
+ }
208
+ migrationHash = migration .Digest
209
+ if exists && ! recreate {
210
+ // We might still need to recreate the database if the schema has changed
211
+ existing := ""
212
+ err := conn .QueryRow ("SELECT migration FROM migrations WHERE db=$1" , dbName ).Scan (& existing )
213
+ if err != nil {
214
+ if ! errors .Is (err , sql .ErrNoRows ) {
215
+ return nil , fmt .Errorf ("failed to query migrations table: %w" , err )
216
+ }
217
+ } else {
218
+ if existing != migrationHash {
219
+ logger .Infof ("Recreating database %q due to schema change" , dbName ) //nolint
220
+ recreate = true
221
+ }
222
+ }
223
+ }
224
+ }
225
+
153
226
if exists && recreate {
154
227
// Terminate any dangling connections.
155
228
_ , err = conn .ExecContext (ctx , `
@@ -172,6 +245,12 @@ func provisionPostgres(postgresPort int, recreate bool) InMemResourceProvisioner
172
245
}
173
246
}
174
247
248
+ if migrationHash != "" {
249
+ _ , err := conn .Exec ("INSERT INTO migrations (db, migration) VALUES ($1, $2)ON CONFLICT (db) DO UPDATE SET migration = EXCLUDED.migration;" , dbName , migrationHash )
250
+ if err != nil {
251
+ return nil , fmt .Errorf ("failed to insert migration hash: %w" , err )
252
+ }
253
+ }
175
254
dsn := dsn .PostgresDSN (dbName , dsn .Port (postgresPort ))
176
255
177
256
return & schema.RuntimeElement {
0 commit comments