2022-12-02 20:40:58 -07:00
package database
import (
"fmt"
2024-08-29 05:19:27 -06:00
"net/url"
"strings"
2023-08-27 18:09:46 -06:00
"github.com/fastenhealth/fasten-onprem/backend/pkg/config"
2023-09-09 09:24:25 -06:00
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
2022-12-02 20:40:58 -07:00
"github.com/sirupsen/logrus"
2023-11-07 16:17:00 -07:00
//"github.com/glebarez/sqlite"
"gorm.io/driver/sqlite"
2022-12-02 20:40:58 -07:00
"gorm.io/gorm"
)
2023-11-07 16:17:00 -07:00
// uses github.com/mattn/go-sqlite3 driver (warning, uses CGO)
2023-10-12 15:19:12 -06:00
func newSqliteRepository ( appConfig config . Interface , globalLogger logrus . FieldLogger , eventBus event_bus . Interface ) ( DatabaseRepository , error ) {
2022-12-02 20:40:58 -07:00
//backgroundContext := context.Background()
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Gorm/SQLite setup
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
globalLogger . Infof ( "Trying to connect to sqlite db: %s\n" , appConfig . GetString ( "database.location" ) )
2023-10-08 17:29:26 -06:00
// BUSY TIMEOUT SETTING DOCS ---
2022-12-02 20:40:58 -07:00
// When a transaction cannot lock the database, because it is already locked by another one,
// SQLite by default throws an error: database is locked. This behavior is usually not appropriate when
// concurrent access is needed, typically when multiple processes write to the same database.
// PRAGMA busy_timeout lets you set a timeout or a handler for these events. When setting a timeout,
// SQLite will try the transaction multiple times within this timeout.
// fixes #341
// https://rsqlite.r-dbi.org/reference/sqlitesetbusyhandler
// retrying for 30000 milliseconds, 30seconds - this would be unreasonable for a distributed multi-tenant application,
// but should be fine for local usage.
2023-10-08 17:29:26 -06:00
//
// JOURNAL MODE WAL DOCS ---
//
// Write-Ahead Logging or WAL (New Way)
// In this case all writes are appended to a temporary file (write-ahead log) and this file is periodically merged with the original database. When SQLite is searching for something it would first check this temporary file and if nothing is found proceed with the main database file.
// As a result, readers don’ t compete with writers and performance is much better compared to the Old Way.
// https://stackoverflow.com/questions/4060772/sqlite-concurrent-access
2023-11-07 16:17:00 -07:00
//
// NOTE: this schema is driver specific, and may not work with other drivers.
// eg.https://github.com/mattn/go-sqlite3 uses `?_journal_mode=WAL` prefixes
// https://github.com/glebarez/sqlite uses `?_pragma=journal_mode(WAL)`
// see https://github.com/mattn/go-sqlite3/compare/master...jgiannuzzi:go-sqlite3:sqlite3mc
// see https://github.com/mattn/go-sqlite3/pull/1109
pragmaOpts := map [ string ] string {
"_busy_timeout" : "5000" ,
"_foreign_keys" : "on" ,
"_journal_mode" : "WAL" ,
}
//validation of encryption key happens in ValidateConfig method
if appConfig . IsSet ( "database.encryption.key" ) {
pragmaOpts [ "_cipher" ] = "sqlcipher"
pragmaOpts [ "_legacy" ] = "3"
pragmaOpts [ "_hmac_use" ] = "off"
pragmaOpts [ "_kdf_iter" ] = "4000"
pragmaOpts [ "_legacy_page_size" ] = "1024"
pragmaOpts [ "_key" ] = appConfig . GetString ( "database.encryption.key" )
}
pragmaStr := sqlitePragmaString ( pragmaOpts )
2023-10-08 17:29:26 -06:00
dsn := "file:" + appConfig . GetString ( "database.location" ) + pragmaStr
database , err := gorm . Open ( sqlite . Open ( dsn ) , & gorm . Config {
2022-12-02 20:40:58 -07:00
//TODO: figure out how to log database queries again.
2023-09-20 14:57:12 -06:00
//logger: logger
2022-12-02 20:40:58 -07:00
DisableForeignKeyConstraintWhenMigrating : true ,
} )
2023-11-07 16:17:00 -07:00
if err != nil {
if strings . Contains ( err . Error ( ) , "file is not a database" ) {
return nil , fmt . Errorf ( "failed to connect to database! encryption key may be incorrect - %w" , err )
}
return nil , fmt . Errorf ( "failed to connect to database! - %w" , err )
}
2022-12-02 20:40:58 -07:00
if strings . ToUpper ( appConfig . GetString ( "log.level" ) ) == "DEBUG" {
database = database . Debug ( ) //set debug globally
}
2023-10-08 17:29:26 -06:00
globalLogger . Infof ( "Successfully connected to fasten sqlite db: %s\n" , dsn )
////verify journal mode
//var journalMode []map[string]interface{}
//resp := database.Raw("PRAGMA journal_mode;").Scan(&journalMode)
//if resp.Error != nil {
// return nil, fmt.Errorf("Failed to verify journal mode! - %v", resp.Error)
//} else {
// globalLogger.Infof("Journal mode: %v", journalMode)
//}
2022-12-02 20:40:58 -07:00
2023-10-13 17:57:43 -06:00
fastenRepo := GormRepository {
2022-12-03 13:48:35 -07:00
AppConfig : appConfig ,
Logger : globalLogger ,
GormClient : database ,
2023-09-20 14:57:12 -06:00
EventBus : eventBus ,
2022-12-03 13:48:35 -07:00
}
2023-03-21 09:04:43 -06:00
err = fastenRepo . Migrate ( )
2022-12-02 20:40:58 -07:00
if err != nil {
2022-12-03 13:48:35 -07:00
return nil , err
2022-12-02 20:40:58 -07:00
}
2023-10-09 18:16:18 -06:00
//fail any Locked jobs. This is necessary because the job may have been locked by a process that was killed.
err = fastenRepo . CancelAllLockedBackgroundJobsAndFail ( )
if err != nil {
return nil , err
}
2023-03-21 09:04:43 -06:00
return & fastenRepo , nil
2022-12-02 20:40:58 -07:00
}
2023-11-07 16:17:00 -07:00
func sqlitePragmaString ( pragmas map [ string ] string ) string {
q := url . Values { }
for key , val := range pragmas {
//q.Add("_pragma", fmt.Sprintf("%s=%s", key, val))
q . Add ( key , val )
}
queryStr := q . Encode ( )
if len ( queryStr ) > 0 {
return "?" + queryStr
}
return ""
}