2023-03-12 18:00:57 +03:00
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
2021-03-02 20:26:30 +03:00
2021-08-25 16:34:33 +03:00
package bundb
2021-03-02 20:26:30 +03:00
import (
"context"
2021-07-19 19:03:07 +03:00
"crypto/tls"
"crypto/x509"
2021-08-25 16:34:33 +03:00
"database/sql"
2021-07-19 19:03:07 +03:00
"encoding/pem"
2021-03-02 20:26:30 +03:00
"errors"
"fmt"
2024-10-16 15:13:58 +03:00
"math"
2023-09-01 16:13:33 +03:00
"net/url"
2021-07-19 19:03:07 +03:00
"os"
2021-09-20 19:20:21 +03:00
"runtime"
2021-03-03 20:12:02 +03:00
"strings"
2021-03-03 00:52:31 +03:00
"time"
2021-03-02 20:26:30 +03:00
2023-01-17 15:29:44 +03:00
"codeberg.org/gruf/go-bytesize"
2022-08-22 12:21:36 +03:00
"github.com/google/uuid"
2023-05-12 15:33:40 +03:00
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/stdlib"
2021-04-01 21:46:45 +03:00
"github.com/superseriousbusiness/gotosocial/internal/config"
2021-05-15 12:58:11 +03:00
"github.com/superseriousbusiness/gotosocial/internal/db"
2021-08-31 20:27:02 +03:00
"github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations"
2021-05-08 15:25:55 +03:00
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
2022-07-19 11:47:55 +03:00
"github.com/superseriousbusiness/gotosocial/internal/log"
2023-11-20 18:43:55 +03:00
"github.com/superseriousbusiness/gotosocial/internal/metrics"
2022-12-08 20:35:14 +03:00
"github.com/superseriousbusiness/gotosocial/internal/state"
2023-05-09 20:19:48 +03:00
"github.com/superseriousbusiness/gotosocial/internal/tracing"
2021-08-25 16:34:33 +03:00
"github.com/uptrace/bun"
2023-12-16 14:54:53 +03:00
"github.com/uptrace/bun/dialect"
2021-08-25 16:34:33 +03:00
"github.com/uptrace/bun/dialect/pgdialect"
2021-08-29 17:41:41 +03:00
"github.com/uptrace/bun/dialect/sqlitedialect"
2021-08-31 20:27:02 +03:00
"github.com/uptrace/bun/migrate"
2021-08-25 16:34:33 +03:00
)
2022-09-26 12:56:01 +03:00
// DBService satisfies the DB interface
type DBService struct {
2021-08-20 13:26:56 +03:00
db . Account
db . Admin
2024-07-23 22:44:31 +03:00
db . AdvancedMigration
2023-08-10 17:08:41 +03:00
db . Application
2021-08-20 13:26:56 +03:00
db . Basic
2024-07-23 22:44:31 +03:00
db . Conversation
2021-08-20 13:26:56 +03:00
db . Domain
2022-05-20 11:34:36 +03:00
db . Emoji
2023-12-18 17:18:25 +03:00
db . HeaderFilter
2021-08-20 13:26:56 +03:00
db . Instance
2024-07-11 17:44:29 +03:00
db . Interaction
2024-03-06 13:15:58 +03:00
db . Filter
2023-05-25 11:37:38 +03:00
db . List
2023-07-29 13:49:14 +03:00
db . Marker
2021-08-20 13:26:56 +03:00
db . Media
db . Mention
2024-03-06 13:18:57 +03:00
db . Move
2021-08-20 13:26:56 +03:00
db . Notification
2023-11-08 17:32:17 +03:00
db . Poll
2021-08-20 13:26:56 +03:00
db . Relationship
2023-01-10 17:19:05 +03:00
db . Report
2023-08-19 15:33:15 +03:00
db . Rule
2023-06-21 19:26:40 +03:00
db . Search
2021-08-25 16:34:33 +03:00
db . Session
2024-09-10 15:34:49 +03:00
db . SinBinStatus
2021-08-20 13:26:56 +03:00
db . Status
2023-03-20 21:10:08 +03:00
db . StatusBookmark
db . StatusFave
2023-07-31 16:47:35 +03:00
db . Tag
2023-10-25 17:04:53 +03:00
db . Thread
2021-08-20 13:26:56 +03:00
db . Timeline
2022-10-03 11:46:11 +03:00
db . User
2022-11-11 14:18:38 +03:00
db . Tombstone
2024-07-30 14:58:31 +03:00
db . WorkerTask
2024-02-07 17:43:27 +03:00
db * bun . DB
2021-03-02 20:26:30 +03:00
}
2023-07-25 11:34:05 +03:00
// GetDB returns the underlying database connection pool.
2022-09-26 12:56:01 +03:00
// Should only be used in testing + exceptional circumstance.
2024-02-07 17:43:27 +03:00
func ( dbService * DBService ) DB ( ) * bun . DB {
2023-07-25 11:34:05 +03:00
return dbService . db
2022-09-26 12:56:01 +03:00
}
2021-10-11 15:37:33 +03:00
func doMigration ( ctx context . Context , db * bun . DB ) error {
2021-08-31 20:27:02 +03:00
migrator := migrate . NewMigrator ( db , migrations . Migrations )
if err := migrator . Init ( ctx ) ; err != nil {
return err
}
group , err := migrator . Migrate ( ctx )
2023-05-12 15:33:40 +03:00
if err != nil && ! strings . Contains ( err . Error ( ) , "no migrations" ) {
2021-08-31 20:27:02 +03:00
return err
}
2023-05-12 15:33:40 +03:00
if group == nil || group . ID == 0 {
2023-02-17 14:02:29 +03:00
log . Info ( ctx , "there are no new migrations to run" )
2021-08-31 20:27:02 +03:00
return nil
}
2023-02-17 14:02:29 +03:00
log . Infof ( ctx , "MIGRATED DATABASE TO %s" , group )
2023-12-16 14:54:53 +03:00
if db . Dialect ( ) . Name ( ) == dialect . SQLite {
2024-03-14 20:40:36 +03:00
log . Info ( ctx ,
"running ANALYZE to update table and index statistics; this will take somewhere between " +
"1-10 minutes, or maybe longer depending on your hardware and database size, please be patient" ,
)
2023-12-16 14:54:53 +03:00
_ , err := db . ExecContext ( ctx , "ANALYZE" )
if err != nil {
log . Warnf ( ctx , "ANALYZE failed, query planner may make poor life choices: %s" , err )
}
}
2021-08-31 20:27:02 +03:00
return nil
}
2021-08-25 16:34:33 +03:00
// NewBunDBService returns a bunDB derived from the provided config, which implements the go-fed DB interface.
// Under the hood, it uses https://github.com/uptrace/bun to create and maintain a database connection.
2022-12-08 20:35:14 +03:00
func NewBunDBService ( ctx context . Context , state * state . State ) ( db . DB , error ) {
2024-02-07 17:43:27 +03:00
var db * bun . DB
2021-11-21 19:41:51 +03:00
var err error
2023-01-17 15:29:44 +03:00
t := strings . ToLower ( config . GetDbType ( ) )
2021-12-07 15:31:39 +03:00
2023-01-17 15:29:44 +03:00
switch t {
case "postgres" :
2024-05-27 18:46:15 +03:00
db , err = pgConn ( ctx )
2021-08-25 16:34:33 +03:00
if err != nil {
2021-11-21 19:41:51 +03:00
return nil , err
2021-08-25 16:34:33 +03:00
}
2023-01-17 15:29:44 +03:00
case "sqlite" :
2024-05-27 18:46:15 +03:00
db , err = sqliteConn ( ctx )
2021-08-29 17:41:41 +03:00
if err != nil {
2021-11-21 19:41:51 +03:00
return nil , err
2021-08-29 17:41:41 +03:00
}
2021-08-25 16:34:33 +03:00
default :
2023-01-17 15:29:44 +03:00
return nil , fmt . Errorf ( "database type %s not supported for bundb" , t )
2021-03-05 20:31:12 +03:00
}
2023-05-12 15:33:40 +03:00
// Add database query hooks.
2023-07-25 11:34:05 +03:00
db . AddQueryHook ( queryHook { } )
2023-05-09 20:19:48 +03:00
if config . GetTracingEnabled ( ) {
2023-07-25 11:34:05 +03:00
db . AddQueryHook ( tracing . InstrumentBun ( ) )
2023-05-09 20:19:48 +03:00
}
2023-11-20 18:43:55 +03:00
if config . GetMetricsEnabled ( ) {
db . AddQueryHook ( metrics . InstrumentBun ( ) )
}
2023-05-09 20:19:48 +03:00
2021-11-21 19:41:51 +03:00
// table registration is needed for many-to-many, see:
// https://bun.uptrace.dev/orm/many-to-many-relation/
2024-02-07 17:43:27 +03:00
for _ , t := range [ ] interface { } {
& gtsmodel . AccountToEmoji { } ,
2024-07-23 22:44:31 +03:00
& gtsmodel . ConversationToStatus { } ,
2024-02-07 17:43:27 +03:00
& gtsmodel . StatusToEmoji { } ,
& gtsmodel . StatusToTag { } ,
& gtsmodel . ThreadToStatus { } ,
} {
2023-07-25 11:34:05 +03:00
db . RegisterModel ( t )
2021-03-03 00:52:31 +03:00
}
2021-11-21 19:41:51 +03:00
// perform any pending database migrations: this includes
// the very first 'migration' on startup which just creates
// necessary tables
2024-02-07 17:43:27 +03:00
if err := doMigration ( ctx , db ) ; err != nil {
2021-08-31 20:27:02 +03:00
return nil , fmt . Errorf ( "db migration error: %s" , err )
}
2021-09-01 12:08:21 +03:00
2022-09-26 12:56:01 +03:00
ps := & DBService {
2022-12-08 20:35:14 +03:00
Account : & accountDB {
2023-07-25 11:34:05 +03:00
db : db ,
2022-12-08 20:35:14 +03:00
state : state ,
} ,
2021-08-20 13:26:56 +03:00
Admin : & adminDB {
2023-07-25 11:34:05 +03:00
db : db ,
2022-12-08 20:35:14 +03:00
state : state ,
2021-08-20 13:26:56 +03:00
} ,
2024-07-23 22:44:31 +03:00
AdvancedMigration : & advancedMigrationDB {
db : db ,
state : state ,
} ,
2023-08-10 17:08:41 +03:00
Application : & applicationDB {
db : db ,
state : state ,
} ,
2021-08-20 13:26:56 +03:00
Basic : & basicDB {
2023-07-25 11:34:05 +03:00
db : db ,
2021-08-20 13:26:56 +03:00
} ,
2024-07-23 22:44:31 +03:00
Conversation : & conversationDB {
db : db ,
state : state ,
} ,
2022-12-08 20:35:14 +03:00
Domain : & domainDB {
2023-07-25 11:34:05 +03:00
db : db ,
2022-12-08 20:35:14 +03:00
state : state ,
} ,
Emoji : & emojiDB {
2023-07-25 11:34:05 +03:00
db : db ,
2022-12-08 20:35:14 +03:00
state : state ,
} ,
2023-12-18 17:18:25 +03:00
HeaderFilter : & headerFilterDB {
db : db ,
state : state ,
} ,
2021-08-20 13:26:56 +03:00
Instance : & instanceDB {
2023-07-25 11:34:05 +03:00
db : db ,
2023-07-07 12:34:12 +03:00
state : state ,
2021-08-20 13:26:56 +03:00
} ,
2024-07-11 17:44:29 +03:00
Interaction : & interactionDB {
db : db ,
state : state ,
} ,
2024-03-06 13:15:58 +03:00
Filter : & filterDB {
db : db ,
state : state ,
} ,
2023-05-25 11:37:38 +03:00
List : & listDB {
2023-07-25 11:34:05 +03:00
db : db ,
2023-05-25 11:37:38 +03:00
state : state ,
} ,
2023-07-29 13:49:14 +03:00
Marker : & markerDB {
db : db ,
state : state ,
} ,
2021-08-20 13:26:56 +03:00
Media : & mediaDB {
2023-07-25 11:34:05 +03:00
db : db ,
2023-02-13 23:19:51 +03:00
state : state ,
2021-08-20 13:26:56 +03:00
} ,
2022-12-08 20:35:14 +03:00
Mention : & mentionDB {
2023-07-25 11:34:05 +03:00
db : db ,
2022-12-08 20:35:14 +03:00
state : state ,
} ,
2024-03-06 13:18:57 +03:00
Move : & moveDB {
db : db ,
state : state ,
} ,
2022-12-08 20:35:14 +03:00
Notification : & notificationDB {
2023-07-25 11:34:05 +03:00
db : db ,
2022-12-08 20:35:14 +03:00
state : state ,
} ,
2023-11-08 17:32:17 +03:00
Poll : & pollDB {
db : db ,
state : state ,
} ,
2022-12-08 20:35:14 +03:00
Relationship : & relationshipDB {
2023-07-25 11:34:05 +03:00
db : db ,
2022-12-08 20:35:14 +03:00
state : state ,
} ,
2023-01-10 17:19:05 +03:00
Report : & reportDB {
2023-07-25 11:34:05 +03:00
db : db ,
2023-01-10 17:19:05 +03:00
state : state ,
} ,
2023-08-19 15:33:15 +03:00
Rule : & ruleDB {
db : db ,
state : state ,
} ,
2023-06-21 19:26:40 +03:00
Search : & searchDB {
2023-07-25 11:34:05 +03:00
db : db ,
2023-06-21 19:26:40 +03:00
state : state ,
} ,
2021-08-25 16:34:33 +03:00
Session : & sessionDB {
2023-07-25 11:34:05 +03:00
db : db ,
2021-08-20 13:26:56 +03:00
} ,
2024-09-10 15:34:49 +03:00
SinBinStatus : & sinBinStatusDB {
db : db ,
state : state ,
} ,
2022-12-08 20:35:14 +03:00
Status : & statusDB {
2023-07-25 11:34:05 +03:00
db : db ,
2022-12-08 20:35:14 +03:00
state : state ,
} ,
2023-03-20 21:10:08 +03:00
StatusBookmark : & statusBookmarkDB {
2023-07-25 11:34:05 +03:00
db : db ,
2023-03-20 21:10:08 +03:00
state : state ,
} ,
StatusFave : & statusFaveDB {
2023-07-25 11:34:05 +03:00
db : db ,
2023-03-20 21:10:08 +03:00
state : state ,
} ,
2023-07-31 16:47:35 +03:00
Tag : & tagDB {
2024-01-19 15:57:29 +03:00
db : db ,
2023-07-31 16:47:35 +03:00
state : state ,
} ,
2023-10-25 17:04:53 +03:00
Thread : & threadDB {
db : db ,
state : state ,
} ,
2022-12-08 20:35:14 +03:00
Timeline : & timelineDB {
2023-07-25 11:34:05 +03:00
db : db ,
2022-12-08 20:35:14 +03:00
state : state ,
} ,
User : & userDB {
2023-07-25 11:34:05 +03:00
db : db ,
2022-12-08 20:35:14 +03:00
state : state ,
} ,
Tombstone : & tombstoneDB {
2023-07-25 11:34:05 +03:00
db : db ,
2022-12-08 20:35:14 +03:00
state : state ,
} ,
2024-07-30 14:58:31 +03:00
WorkerTask : & workerTaskDB {
db : db ,
} ,
2023-07-25 11:34:05 +03:00
db : db ,
2021-04-01 21:46:45 +03:00
}
2021-03-02 20:26:30 +03:00
2021-08-25 16:34:33 +03:00
// we can confidently return this useable service now
2021-04-01 21:46:45 +03:00
return ps , nil
2021-03-23 00:26:54 +03:00
}
2024-05-27 18:46:15 +03:00
func pgConn ( ctx context . Context ) ( * bun . DB , error ) {
2023-01-26 17:12:48 +03:00
opts , err := deriveBunDBPGOptions ( ) //nolint:contextcheck
if err != nil {
2024-02-07 17:43:27 +03:00
return nil , fmt . Errorf ( "could not create bundb postgres options: %w" , err )
2023-01-26 17:12:48 +03:00
}
2024-02-07 17:43:27 +03:00
cfg := stdlib . RegisterConnConfig ( opts )
sqldb , err := sql . Open ( "pgx-gts" , cfg )
if err != nil {
return nil , fmt . Errorf ( "could not open postgres db: %w" , err )
}
2023-01-26 17:12:48 +03:00
// Tune db connections for postgres, see:
// - https://bun.uptrace.dev/guide/running-bun-in-production.html#database-sql
// - https://www.alexedwards.net/blog/configuring-sqldb
sqldb . SetMaxOpenConns ( maxOpenConns ( ) ) // x number of conns per CPU
sqldb . SetMaxIdleConns ( 2 ) // assume default 2; if max idle is less than max open, it will be automatically adjusted
sqldb . SetConnMaxLifetime ( 5 * time . Minute ) // fine to kill old connections
2024-02-07 17:43:27 +03:00
db := bun . NewDB ( sqldb , pgdialect . New ( ) )
2023-01-26 17:12:48 +03:00
// ping to check the db is there and listening
2023-08-17 19:26:21 +03:00
if err := db . PingContext ( ctx ) ; err != nil {
2024-02-07 17:43:27 +03:00
return nil , fmt . Errorf ( "postgres ping: %w" , err )
2023-01-26 17:12:48 +03:00
}
2023-02-17 14:02:29 +03:00
log . Info ( ctx , "connected to POSTGRES database" )
2023-08-17 19:26:21 +03:00
return db , nil
2023-01-26 17:12:48 +03:00
}
2024-05-27 18:46:15 +03:00
func sqliteConn ( ctx context . Context ) ( * bun . DB , error ) {
2022-01-30 19:06:28 +03:00
// validate db address has actually been set
2023-01-17 15:29:44 +03:00
address := config . GetDbAddress ( )
if address == "" {
2022-05-30 15:41:24 +03:00
return nil , fmt . Errorf ( "'%s' was not set when attempting to start sqlite" , config . DbAddressFlag ( ) )
2022-01-30 19:06:28 +03:00
}
2021-12-07 15:31:39 +03:00
2023-09-01 16:13:33 +03:00
// Build SQLite connection address with prefs.
2024-09-08 17:14:56 +03:00
address , inMem := buildSQLiteAddress ( address )
2022-08-22 12:21:36 +03:00
2021-11-21 19:41:51 +03:00
// Open new DB instance
2024-02-07 17:43:27 +03:00
sqldb , err := sql . Open ( "sqlite-gts" , address )
2021-11-21 19:41:51 +03:00
if err != nil {
2023-01-31 15:46:45 +03:00
return nil , fmt . Errorf ( "could not open sqlite db with address %s: %w" , address , err )
2021-11-21 19:41:51 +03:00
}
2023-01-26 17:12:48 +03:00
// Tune db connections for sqlite, see:
// - https://bun.uptrace.dev/guide/running-bun-in-production.html#database-sql
// - https://www.alexedwards.net/blog/configuring-sqldb
2023-07-25 11:34:05 +03:00
sqldb . SetMaxOpenConns ( maxOpenConns ( ) ) // x number of conns per CPU
sqldb . SetMaxIdleConns ( 1 ) // only keep max 1 idle connection around
2024-09-08 17:14:56 +03:00
if inMem {
log . Warn ( nil , "using sqlite in-memory mode; all data will be deleted when gts shuts down; this mode should only be used for debugging or running tests" )
// Don't close aged connections as this may wipe the DB.
sqldb . SetConnMaxLifetime ( 0 )
} else {
sqldb . SetConnMaxLifetime ( 5 * time . Minute )
}
2021-11-21 19:41:51 +03:00
2024-02-07 17:43:27 +03:00
db := bun . NewDB ( sqldb , sqlitedialect . New ( ) )
2021-11-21 19:41:51 +03:00
// ping to check the db is there and listening
2023-08-17 19:26:21 +03:00
if err := db . PingContext ( ctx ) ; err != nil {
2024-02-07 17:43:27 +03:00
return nil , fmt . Errorf ( "sqlite ping: %w" , err )
2021-11-21 19:41:51 +03:00
}
2024-05-27 18:46:15 +03:00
2023-02-17 14:02:29 +03:00
log . Infof ( ctx , "connected to SQLITE database with address %s" , address )
2023-01-17 15:29:44 +03:00
2023-08-17 19:26:21 +03:00
return db , nil
2021-11-21 19:41:51 +03:00
}
2021-03-02 20:26:30 +03:00
/ *
HANDY STUFF
* /
2023-01-26 17:12:48 +03:00
// maxOpenConns returns multiplier * GOMAXPROCS,
2023-01-31 15:46:45 +03:00
// returning just 1 instead if multiplier < 1.
2023-01-26 17:12:48 +03:00
func maxOpenConns ( ) int {
multiplier := config . GetDbMaxOpenConnsMultiplier ( )
if multiplier < 1 {
2023-01-31 15:46:45 +03:00
return 1
2023-01-26 17:12:48 +03:00
}
2024-10-14 01:19:52 +03:00
// Specifically for SQLite databases with
// a journal mode of anything EXCEPT "wal",
// only 1 concurrent connection is supported.
if strings . ToLower ( config . GetDbType ( ) ) == "sqlite" {
journalMode := config . GetDbSqliteJournalMode ( )
journalMode = strings . ToLower ( journalMode )
if journalMode != "wal" {
return 1
}
}
2023-01-26 17:12:48 +03:00
return multiplier * runtime . GOMAXPROCS ( 0 )
}
2021-08-25 16:34:33 +03:00
// deriveBunDBPGOptions takes an application config and returns either a ready-to-use set of options
2021-03-02 20:26:30 +03:00
// with sensible defaults, or an error if it's not satisfied by the provided config.
2021-12-07 15:31:39 +03:00
func deriveBunDBPGOptions ( ) ( * pgx . ConnConfig , error ) {
2024-11-25 18:15:33 +03:00
// If database URL is defined, ignore
// other DB-related configuration fields.
if url := config . GetDbPostgresConnectionString ( ) ; url != "" {
return pgx . ParseConfig ( url )
2024-08-08 15:00:19 +03:00
}
2024-11-25 18:15:33 +03:00
2021-12-21 14:08:27 +03:00
// these are all optional, the db adapter figures out defaults
2022-05-30 15:41:24 +03:00
address := config . GetDbAddress ( )
2021-03-03 00:52:31 +03:00
// validate database
2022-05-30 15:41:24 +03:00
database := config . GetDbDatabase ( )
2021-12-07 15:31:39 +03:00
if database == "" {
2021-03-04 14:07:24 +03:00
return nil , errors . New ( "no database set" )
2021-03-02 20:26:30 +03:00
}
2021-07-19 19:03:07 +03:00
var tlsConfig * tls . Config
2022-05-30 15:41:24 +03:00
switch config . GetDbTLSMode ( ) {
2023-01-17 15:29:44 +03:00
case "" , "disable" :
2021-07-19 19:03:07 +03:00
break // nothing to do
2023-01-17 15:29:44 +03:00
case "enable" :
2021-07-19 19:03:07 +03:00
tlsConfig = & tls . Config {
2023-10-23 15:07:31 +03:00
InsecureSkipVerify : true , //nolint:gosec
2021-07-19 19:03:07 +03:00
}
2023-01-17 15:29:44 +03:00
case "require" :
2021-07-19 19:03:07 +03:00
tlsConfig = & tls . Config {
InsecureSkipVerify : false ,
2022-05-30 15:41:24 +03:00
ServerName : address ,
2021-11-22 10:46:19 +03:00
MinVersion : tls . VersionTLS12 ,
2021-07-19 19:03:07 +03:00
}
}
2022-05-30 15:41:24 +03:00
if certPath := config . GetDbTLSCACert ( ) ; tlsConfig != nil && certPath != "" {
2021-07-19 19:03:07 +03:00
// load the system cert pool first -- we'll append the given CA cert to this
certPool , err := x509 . SystemCertPool ( )
if err != nil {
return nil , fmt . Errorf ( "error fetching system CA cert pool: %s" , err )
}
// open the file itself and make sure there's something in it
2022-05-30 15:41:24 +03:00
caCertBytes , err := os . ReadFile ( certPath )
2021-07-19 19:03:07 +03:00
if err != nil {
2022-05-30 15:41:24 +03:00
return nil , fmt . Errorf ( "error opening CA certificate at %s: %s" , certPath , err )
2021-07-19 19:03:07 +03:00
}
if len ( caCertBytes ) == 0 {
2022-05-30 15:41:24 +03:00
return nil , fmt . Errorf ( "ca cert at %s was empty" , certPath )
2021-07-19 19:03:07 +03:00
}
// make sure we have a PEM block
caPem , _ := pem . Decode ( caCertBytes )
if caPem == nil {
2022-05-30 15:41:24 +03:00
return nil , fmt . Errorf ( "could not parse cert at %s into PEM" , certPath )
2021-07-19 19:03:07 +03:00
}
// parse the PEM block into the certificate
caCert , err := x509 . ParseCertificate ( caPem . Bytes )
if err != nil {
2024-02-07 17:43:27 +03:00
return nil , fmt . Errorf ( "could not parse cert at %s into x509 certificate: %w" , certPath , err )
2021-07-19 19:03:07 +03:00
}
// we're happy, add it to the existing pool and then use this pool in our tls config
certPool . AddCert ( caCert )
tlsConfig . RootCAs = certPool
}
2021-08-25 16:34:33 +03:00
cfg , _ := pgx . ParseConfig ( "" )
2021-12-21 14:08:27 +03:00
if address != "" {
cfg . Host = address
}
2022-06-03 16:40:38 +03:00
if port := config . GetDbPort ( ) ; port > 0 {
2024-10-16 15:13:58 +03:00
if port > math . MaxUint16 {
return nil , errors . New ( "invalid port, must be in range 1-65535" )
}
cfg . Port = uint16 ( port ) // #nosec G115 -- Just validated above.
2021-12-21 14:08:27 +03:00
}
2022-05-30 15:41:24 +03:00
if u := config . GetDbUser ( ) ; u != "" {
cfg . User = u
2021-12-21 14:08:27 +03:00
}
2022-05-30 15:41:24 +03:00
if p := config . GetDbPassword ( ) ; p != "" {
cfg . Password = p
2021-12-21 14:08:27 +03:00
}
if tlsConfig != nil {
cfg . TLSConfig = tlsConfig
}
2021-12-07 15:31:39 +03:00
cfg . Database = database
2022-05-30 15:41:24 +03:00
cfg . RuntimeParams [ "application_name" ] = config . GetApplicationName ( )
2021-03-02 20:26:30 +03:00
2021-08-25 16:34:33 +03:00
return cfg , nil
2021-03-02 20:26:30 +03:00
}
2023-09-01 16:13:33 +03:00
// buildSQLiteAddress will build an SQLite address string from given config input,
// appending user defined SQLite connection preferences (e.g. cache_size, journal_mode etc).
2024-09-08 17:14:56 +03:00
// The returned bool indicates whether this is an in-memory address or not.
func buildSQLiteAddress ( addr string ) ( string , bool ) {
2023-09-01 16:13:33 +03:00
// Notes on SQLite preferences:
//
// - SQLite by itself supports setting a subset of its configuration options
// via URI query arguments in the connection. Namely `mode` and `cache`.
// This is the same situation for the directly transpiled C->Go code in
// modernc.org/sqlite, i.e. modernc.org/sqlite/lib, NOT the Go SQL driver.
//
// - `modernc.org/sqlite` has a "shim" around it to allow the directly
// transpiled C code to be usable with a more native Go API. This is in
// the form of a `database/sql/driver.Driver{}` implementation that calls
// through to the transpiled C code.
//
// - The SQLite shim we interface with adds support for setting ANY of the
// configuration options via query arguments, through using a special `_pragma`
// query key that specifies SQLite PRAGMAs to set upon opening each connection.
// As such you will see below that most config is set with the `_pragma` key.
//
// - As for why we're setting these PRAGMAs by connection string instead of
// directly executing the PRAGMAs ourselves? That's to ensure that all of
// configuration options are set across _all_ of our SQLite connections, given
// that we are a multi-threaded (not directly in a C way) application and that
// each connection is a separate SQLite instance opening the same database.
// And the `database/sql` package provides transparent connection pooling.
// Some data is shared between connections, for example the `journal_mode`
// as that is set in a bit of the file header, but to be sure with the other
// settings we just add them all to the connection URI string.
//
// - We specifically set the `busy_timeout` PRAGMA before the `journal_mode`.
// When Write-Ahead-Logging (WAL) is enabled, in order to handle the issues
// that may arise between separate concurrent read/write threads racing for
// the same database file (and write-ahead log), SQLite will sometimes return
// an `SQLITE_BUSY` error code, which indicates that the query was aborted
// due to a data race and must be retried. The `busy_timeout` PRAGMA configures
// a function handler that SQLite can use internally to handle these data races,
// in that it will attempt to retry the query until the `busy_timeout` time is
// reached. And for whatever reason (:shrug:) SQLite is very particular about
// setting this BEFORE the `journal_mode` is set, otherwise you can end up
// running into more of these `SQLITE_BUSY` return codes than you might expect.
//
// - One final thing (I promise!): `SQLITE_BUSY` is only handled by the internal
// `busy_timeout` handler in the case that a data race occurs contending for
// table locks. THERE ARE STILL OTHER SITUATIONS IN WHICH THIS MAY BE RETURNED!
// As such, we use our wrapping DB{} and Tx{} types (in "db.go") which make use
// of our own retry-busy handler.
// Drop anything fancy from DB address
addr = strings . Split ( addr , "?" ) [ 0 ] // drop any provided query strings
addr = strings . TrimPrefix ( addr , "file:" ) // we'll prepend this later ourselves
// build our own SQLite preferences
// as a series of URL encoded values
prefs := make ( url . Values )
// use immediate transaction lock mode to fail quickly if tx can't lock
// see https://pkg.go.dev/modernc.org/sqlite#Driver.Open
prefs . Add ( "_txlock" , "immediate" )
2024-09-08 17:14:56 +03:00
inMem := false
2023-09-01 16:13:33 +03:00
if addr == ":memory:" {
// Use random name for in-memory instead of ':memory:', so
// multiple in-mem databases can be created without conflict.
2024-09-08 17:14:56 +03:00
inMem = true
2024-05-27 18:46:15 +03:00
addr = "/" + uuid . NewString ( )
prefs . Add ( "vfs" , "memdb" )
2023-09-01 16:13:33 +03:00
}
if dur := config . GetDbSqliteBusyTimeout ( ) ; dur > 0 {
// Set the user provided SQLite busy timeout
// NOTE: MUST BE SET BEFORE THE JOURNAL MODE.
prefs . Add ( "_pragma" , fmt . Sprintf ( "busy_timeout(%d)" , dur . Milliseconds ( ) ) )
}
2023-01-26 17:12:48 +03:00
if mode := config . GetDbSqliteJournalMode ( ) ; mode != "" {
2023-09-01 16:13:33 +03:00
// Set the user provided SQLite journal mode.
prefs . Add ( "_pragma" , fmt . Sprintf ( "journal_mode(%s)" , mode ) )
2023-01-26 17:12:48 +03:00
}
if mode := config . GetDbSqliteSynchronous ( ) ; mode != "" {
2023-09-01 16:13:33 +03:00
// Set the user provided SQLite synchronous mode.
prefs . Add ( "_pragma" , fmt . Sprintf ( "synchronous(%s)" , mode ) )
2023-01-26 17:12:48 +03:00
}
2023-09-01 16:13:33 +03:00
if sz := config . GetDbSqliteCacheSize ( ) ; sz > 0 {
2023-01-26 17:12:48 +03:00
// Set the user provided SQLite cache size (in kibibytes)
// Prepend a '-' character to this to indicate to sqlite
// that we're giving kibibytes rather than num pages.
// https://www.sqlite.org/pragma.html#pragma_cache_size
2023-09-01 16:13:33 +03:00
prefs . Add ( "_pragma" , fmt . Sprintf ( "cache_size(-%d)" , uint64 ( sz / bytesize . KiB ) ) )
2023-01-26 17:12:48 +03:00
}
2023-09-01 16:13:33 +03:00
var b strings . Builder
b . WriteString ( "file:" )
b . WriteString ( addr )
b . WriteString ( "?" )
b . WriteString ( prefs . Encode ( ) )
2024-09-08 17:14:56 +03:00
return b . String ( ) , inMem
2023-01-26 17:12:48 +03:00
}