Skip to content

Commit 8d61aba

Browse files
authored
Feature/script migrations (#44)
* base for script migrations * not going to modify this anymore * mod update * update database on the fly * base tests * this should have been here fromt the start * cleaner to read * fingers crossed * tweaks * run go test * name lol * wooooops * versionnnnnnn * split github actions * jesus * third time is the charm * bruhhhh * yea boi * yea boii 2 * why was this added * welcome back sum * meh
1 parent e18ccbf commit 8d61aba

File tree

17 files changed

+697
-51
lines changed

17 files changed

+697
-51
lines changed

.github/workflows/backend.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Api Tests
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
types:
8+
- opened
9+
- synchronize
10+
11+
permissions:
12+
contents: write
13+
pull-requests: write
14+
15+
jobs:
16+
api_test:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Set up Go
22+
uses: actions/setup-go@v4
23+
with:
24+
go-version: '1.24.3'
25+
cache-dependency-path: MyMusicBoxApi/go.sum
26+
27+
- name: Tidy modules (sync go.mod & go.sum)
28+
working-directory: MyMusicBoxApi
29+
run: go mod tidy
30+
31+
- name: Test
32+
working-directory: MyMusicBoxApi
33+
run: go test -v ./...

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
/bin/
1414
/pkg/
1515

16-
# Go modules
17-
go.sum
1816

1917
# IDE/editor files
2018
.vscode/
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package configuration
2+
3+
import (
4+
"flag"
5+
"musicboxapi/models"
6+
"os"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestLoadConfiguration(t *testing.T) {
13+
// Arrange
14+
// Reset flags
15+
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
16+
17+
os.Args = []string{"cmd", "-port=8081", "-devurl", "-sourceFolder=dev_music", "-outputExtension=opus"}
18+
19+
// Act
20+
LoadConfiguration()
21+
22+
// Assert
23+
var config models.Config = Config
24+
25+
assert.Equal(t, "8081", config.DevPort)
26+
assert.Equal(t, "opus", config.OutputExtension)
27+
assert.Equal(t, "dev_music", config.SourceFolder)
28+
assert.Equal(t, true, config.UseDevUrl)
29+
}

MyMusicBoxApi/database/db.go

Lines changed: 110 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,23 @@ import (
77
"musicboxapi/configuration"
88
"musicboxapi/logging"
99
"os"
10+
"path/filepath"
11+
"strconv"
1012
"strings"
1113
"time"
1214

1315
_ "github.com/lib/pq"
1416
)
1517

18+
const ReturningIdParameter = "RETURNING"
19+
const ReturningIdParameterLower = "returning"
20+
const DatabaseDriver = "postgres"
21+
const MigrationFolder = "migration_scripts"
22+
const MaxOpenConnections = 10
23+
const MaxIdleConnections = 5
24+
const MaxConnectionIdleTimeInMinutes = 10
25+
const MaxConnectionLifeTimeInMinutes = 10
26+
1627
var DbInstance *sql.DB
1728

1829
type BaseTable struct {
@@ -29,10 +40,12 @@ func CreateDatabasConnectionPool() error {
2940

3041
// Will throw an error if its missing a method implementation from interface
3142
// will throw a compile time error
43+
// Should create test for these?
3244
var _ ISongTable = (*SongTable)(nil)
3345
var _ IPlaylistTable = (*PlaylistTable)(nil)
3446
var _ IPlaylistsongTable = (*PlaylistsongTable)(nil)
3547
var _ ITasklogTable = (*TasklogTable)(nil)
48+
var _ IMigrationTable = (*MigrationTable)(nil)
3649

3750
baseConnectionString := "user=postgres dbname=postgres password=%s %s sslmode=disable"
3851
password := os.Getenv("POSTGRES_PASSWORD")
@@ -44,17 +57,18 @@ func CreateDatabasConnectionPool() error {
4457

4558
connectionString := fmt.Sprintf(baseConnectionString, password, host)
4659

47-
DB, err := sql.Open("postgres", connectionString)
60+
DB, err := sql.Open(DatabaseDriver, connectionString)
4861

4962
if err != nil {
5063
logging.Error(fmt.Sprintf("Failed to init database connection: %s", err.Error()))
64+
logging.ErrorStackTrace(err)
5165
return err
5266
}
5367

54-
DB.SetMaxOpenConns(10)
55-
DB.SetMaxIdleConns(5)
56-
DB.SetConnMaxIdleTime(1 * time.Minute)
57-
DB.SetConnMaxLifetime(5 * time.Minute)
68+
DB.SetMaxOpenConns(MaxOpenConnections)
69+
DB.SetMaxIdleConns(MaxIdleConnections)
70+
DB.SetConnMaxIdleTime(MaxConnectionIdleTimeInMinutes * time.Minute)
71+
DB.SetConnMaxLifetime(MaxConnectionLifeTimeInMinutes * time.Minute)
5872

5973
DbInstance = DB
6074

@@ -64,8 +78,7 @@ func CreateDatabasConnectionPool() error {
6478
// Base methods
6579
func (base *BaseTable) InsertWithReturningId(query string, params ...any) (lastInsertedId int, err error) {
6680

67-
if !strings.Contains(query, "RETURNING") {
68-
logging.Error("Query does not contain RETURNING keyword")
81+
if !strings.Contains(query, ReturningIdParameter) {
6982
return -1, errors.New("Query does not contain RETURNING keyword")
7083
}
7184

@@ -74,43 +87,45 @@ func (base *BaseTable) InsertWithReturningId(query string, params ...any) (lastI
7487
statement, err := transaction.Prepare(query)
7588

7689
if err != nil {
77-
transaction.Rollback()
78-
logging.Error(fmt.Sprintf("Prepared statement error: %s", err.Error()))
90+
logging.ErrorStackTrace(err)
7991
return -1, err
8092
}
8193
defer statement.Close()
8294

8395
err = statement.QueryRow(params...).Scan(&lastInsertedId)
8496

8597
if err != nil {
86-
logging.Error(fmt.Sprintf("Queryrow error: %s", err.Error()))
98+
logging.ErrorStackTrace(err)
8799
transaction.Rollback()
88100
return -1, err
89101
}
90102

91103
err = transaction.Commit()
92104

93105
if err != nil {
94-
logging.Error(fmt.Sprintf("Transaction commit error: %s", err.Error()))
106+
logging.ErrorStackTrace(err)
95107
transaction.Rollback()
96108
return -1, err
97109
}
98110

99111
return lastInsertedId, nil
100112
}
113+
101114
func (base *BaseTable) NonScalarQuery(query string, params ...any) (error error) {
102115

103116
transaction, err := base.DB.Begin()
104117

105118
if err != nil {
106119
logging.Error(fmt.Sprintf("Transaction error: %s", err.Error()))
120+
logging.ErrorStackTrace(err)
107121
return err
108122
}
109123

110124
statement, err := transaction.Prepare(query)
111125

112126
if err != nil {
113127
logging.Error(fmt.Sprintf("Prepared statement error: %s", err.Error()))
128+
logging.ErrorStackTrace(err)
114129
return err
115130
}
116131

@@ -119,20 +134,99 @@ func (base *BaseTable) NonScalarQuery(query string, params ...any) (error error)
119134
_, err = statement.Exec(params...)
120135

121136
if err != nil {
122-
logging.Error(fmt.Sprintf("Exec error: %s", err.Error()))
123-
logging.Error(fmt.Sprintf("Query: %s", query))
124-
for index := range params {
125-
logging.Error(params[index])
126-
}
137+
logging.ErrorStackTrace(err)
127138
return err
128139
}
129140

130141
err = transaction.Commit()
131142

132143
if err != nil {
133144
logging.Error(fmt.Sprintf("Transaction commit error: %s", err.Error()))
145+
logging.ErrorStackTrace(err)
134146
return err
135147
}
136148

137149
return nil
138150
}
151+
152+
func ApplyMigrations() {
153+
logging.Info("Applying migrations...")
154+
// files will be sorted by filename
155+
// to make sure the migrations are executed in order
156+
// this naming convention must be used
157+
// 0 initial script.sql
158+
// 1 update column.sql
159+
// etc....
160+
// entries are sorted by file name
161+
dirs, err := os.ReadDir(MigrationFolder)
162+
163+
if err != nil {
164+
logging.ErrorStackTrace(err)
165+
return
166+
}
167+
168+
migrationTable := NewMigrationTableInstance()
169+
170+
currentMigrationFileName, err := migrationTable.GetCurrentAppliedMigrationFileName()
171+
172+
// start at -1 if lastMigrationFileName is empty OR migration table does not exists
173+
// start applying from 0
174+
if currentMigrationFileName == "" || err != nil {
175+
if strings.Contains(err.Error(), `relation "migration" does not exist`) {
176+
logging.Info("First time running database script migrations")
177+
} else {
178+
logging.ErrorStackTrace(err)
179+
return
180+
}
181+
182+
// makes sure we start at script 0
183+
currentMigrationFileName = "-1 nil.sql"
184+
}
185+
186+
currentMigrationFileId, err := strconv.Atoi(strings.Split(currentMigrationFileName, " ")[0])
187+
188+
if err != nil {
189+
logging.ErrorStackTrace(err)
190+
return
191+
}
192+
193+
for _, migrationFile := range dirs {
194+
filePath := filepath.Join(MigrationFolder, migrationFile.Name())
195+
196+
migrationFileId, err := strconv.Atoi(strings.Split(migrationFile.Name(), " ")[0])
197+
198+
if err != nil {
199+
logging.ErrorStackTrace(err)
200+
continue
201+
}
202+
203+
if migrationFileId <= currentMigrationFileId {
204+
continue
205+
}
206+
207+
migrationFileContents, err := os.ReadFile(filePath)
208+
209+
if err != nil {
210+
logging.ErrorStackTrace(err)
211+
continue
212+
}
213+
214+
err = migrationTable.ApplyMigration(string(migrationFileContents))
215+
216+
if err != nil {
217+
logging.Error(fmt.Sprintf("Failed to apply %s", migrationFile.Name()))
218+
logging.ErrorStackTrace(err)
219+
} else {
220+
err = migrationTable.Insert(migrationFile.Name(), string(migrationFileContents))
221+
222+
if err != nil {
223+
logging.Error(fmt.Sprintf("Failed to insert migration entry %s: %s", migrationFile.Name(), err.Error()))
224+
logging.ErrorStackTrace(err)
225+
return
226+
}
227+
228+
logging.Info(fmt.Sprintf("Applied script: %s", migrationFile.Name()))
229+
}
230+
231+
}
232+
}

0 commit comments

Comments
 (0)