@@ -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+
1627var DbInstance * sql.DB
1728
1829type 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
6579func (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+
101114func (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