summaryrefslogtreecommitdiff
path: root/internal/db
diff options
context:
space:
mode:
Diffstat (limited to 'internal/db')
-rw-r--r--internal/db/bundb/admin_test.go1
-rw-r--r--internal/db/bundb/bundb.go25
-rw-r--r--internal/db/bundb/drivers.go346
-rw-r--r--internal/db/bundb/errors.go105
-rw-r--r--internal/db/bundb/tag_test.go13
-rw-r--r--internal/db/error.go4
-rw-r--r--internal/db/postgres/driver.go209
-rw-r--r--internal/db/postgres/errors.go46
-rw-r--r--internal/db/sqlite/driver.go197
-rw-r--r--internal/db/sqlite/driver_wasmsqlite3.go211
-rw-r--r--internal/db/sqlite/errors.go62
-rw-r--r--internal/db/sqlite/errors_wasmsqlite3.go60
-rw-r--r--internal/db/util.go35
13 files changed, 845 insertions, 469 deletions
diff --git a/internal/db/bundb/admin_test.go b/internal/db/bundb/admin_test.go
index da3370c4e..8018ef3fa 100644
--- a/internal/db/bundb/admin_test.go
+++ b/internal/db/bundb/admin_test.go
@@ -74,6 +74,7 @@ func (suite *AdminTestSuite) TestCreateInstanceAccount() {
// we need to take an empty db for this...
testrig.StandardDBTeardown(suite.db)
// ...with tables created but no data
+ suite.db = testrig.NewTestDB(&suite.state)
testrig.CreateTestTables(suite.db)
// make sure there's no instance account in the db yet
diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go
index b0ce575e6..e7256c276 100644
--- a/internal/db/bundb/bundb.go
+++ b/internal/db/bundb/bundb.go
@@ -48,8 +48,6 @@ import (
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/migrate"
-
- "modernc.org/sqlite"
)
// DBService satisfies the DB interface
@@ -133,12 +131,12 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
switch t {
case "postgres":
- db, err = pgConn(ctx, state)
+ db, err = pgConn(ctx)
if err != nil {
return nil, err
}
case "sqlite":
- db, err = sqliteConn(ctx, state)
+ db, err = sqliteConn(ctx)
if err != nil {
return nil, err
}
@@ -295,7 +293,7 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
return ps, nil
}
-func pgConn(ctx context.Context, state *state.State) (*bun.DB, error) {
+func pgConn(ctx context.Context) (*bun.DB, error) {
opts, err := deriveBunDBPGOptions() //nolint:contextcheck
if err != nil {
return nil, fmt.Errorf("could not create bundb postgres options: %w", err)
@@ -326,7 +324,7 @@ func pgConn(ctx context.Context, state *state.State) (*bun.DB, error) {
return db, nil
}
-func sqliteConn(ctx context.Context, state *state.State) (*bun.DB, error) {
+func sqliteConn(ctx context.Context) (*bun.DB, error) {
// validate db address has actually been set
address := config.GetDbAddress()
if address == "" {
@@ -339,9 +337,6 @@ func sqliteConn(ctx context.Context, state *state.State) (*bun.DB, error) {
// Open new DB instance
sqldb, err := sql.Open("sqlite-gts", address)
if err != nil {
- if errWithCode, ok := err.(*sqlite.Error); ok {
- err = errors.New(sqlite.ErrorCodeString[errWithCode.Code()])
- }
return nil, fmt.Errorf("could not open sqlite db with address %s: %w", address, err)
}
@@ -356,11 +351,9 @@ func sqliteConn(ctx context.Context, state *state.State) (*bun.DB, error) {
// ping to check the db is there and listening
if err := db.PingContext(ctx); err != nil {
- if errWithCode, ok := err.(*sqlite.Error); ok {
- err = errors.New(sqlite.ErrorCodeString[errWithCode.Code()])
- }
return nil, fmt.Errorf("sqlite ping: %w", err)
}
+
log.Infof(ctx, "connected to SQLITE database with address %s", address)
return db, nil
@@ -528,12 +521,8 @@ func buildSQLiteAddress(addr string) string {
// Use random name for in-memory instead of ':memory:', so
// multiple in-mem databases can be created without conflict.
- addr = uuid.NewString()
-
- // in-mem-specific preferences
- // (shared cache so that tests don't fail)
- prefs.Add("mode", "memory")
- prefs.Add("cache", "shared")
+ addr = "/" + uuid.NewString()
+ prefs.Add("vfs", "memdb")
}
if dur := config.GetDbSqliteBusyTimeout(); dur > 0 {
diff --git a/internal/db/bundb/drivers.go b/internal/db/bundb/drivers.go
index 1811ad533..f39189c9d 100644
--- a/internal/db/bundb/drivers.go
+++ b/internal/db/bundb/drivers.go
@@ -18,350 +18,14 @@
package bundb
import (
- "context"
"database/sql"
- "database/sql/driver"
- "time"
- _ "unsafe" // linkname shenanigans
- pgx "github.com/jackc/pgx/v5/stdlib"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "modernc.org/sqlite"
+ "github.com/superseriousbusiness/gotosocial/internal/db/postgres"
+ "github.com/superseriousbusiness/gotosocial/internal/db/sqlite"
)
-var (
- // global SQL driver instances.
- postgresDriver = pgx.GetDefaultDriver()
- sqliteDriver = getSQLiteDriver()
-
- // check the postgres connection
- // conforms to our conn{} interface.
- // (note SQLite doesn't export their
- // conn type, and gets checked in
- // tests very regularly anywho).
- _ conn = (*pgx.Conn)(nil)
-)
-
-//go:linkname getSQLiteDriver modernc.org/sqlite.newDriver
-func getSQLiteDriver() *sqlite.Driver
-
func init() {
- sql.Register("pgx-gts", &PostgreSQLDriver{})
- sql.Register("sqlite-gts", &SQLiteDriver{})
-}
-
-// PostgreSQLDriver is our own wrapper around the
-// pgx/stdlib.Driver{} type in order to wrap further
-// SQL driver types with our own err processing.
-type PostgreSQLDriver struct{}
-
-func (d *PostgreSQLDriver) Open(name string) (driver.Conn, error) {
- c, err := postgresDriver.Open(name)
- if err != nil {
- return nil, err
- }
- return &PostgreSQLConn{conn: c.(conn)}, nil
-}
-
-type PostgreSQLConn struct{ conn }
-
-func (c *PostgreSQLConn) Begin() (driver.Tx, error) {
- return c.BeginTx(context.Background(), driver.TxOptions{})
-}
-
-func (c *PostgreSQLConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
- tx, err := c.conn.BeginTx(ctx, opts)
- err = processPostgresError(err)
- if err != nil {
- return nil, err
- }
- return &PostgreSQLTx{tx}, nil
-}
-
-func (c *PostgreSQLConn) Prepare(query string) (driver.Stmt, error) {
- return c.PrepareContext(context.Background(), query)
-}
-
-func (c *PostgreSQLConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
- st, err := c.conn.PrepareContext(ctx, query)
- err = processPostgresError(err)
- if err != nil {
- return nil, err
- }
- return &PostgreSQLStmt{stmt: st.(stmt)}, nil
-}
-
-func (c *PostgreSQLConn) Exec(query string, args []driver.Value) (driver.Result, error) {
- return c.ExecContext(context.Background(), query, toNamedValues(args))
-}
-
-func (c *PostgreSQLConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
- result, err := c.conn.ExecContext(ctx, query, args)
- err = processPostgresError(err)
- return result, err
-}
-
-func (c *PostgreSQLConn) Query(query string, args []driver.Value) (driver.Rows, error) {
- return c.QueryContext(context.Background(), query, toNamedValues(args))
-}
-
-func (c *PostgreSQLConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
- rows, err := c.conn.QueryContext(ctx, query, args)
- err = processPostgresError(err)
- return rows, err
-}
-
-func (c *PostgreSQLConn) Close() error {
- return c.conn.Close()
-}
-
-type PostgreSQLTx struct{ driver.Tx }
-
-func (tx *PostgreSQLTx) Commit() error {
- err := tx.Tx.Commit()
- return processPostgresError(err)
-}
-
-func (tx *PostgreSQLTx) Rollback() error {
- err := tx.Tx.Rollback()
- return processPostgresError(err)
-}
-
-type PostgreSQLStmt struct{ stmt }
-
-func (stmt *PostgreSQLStmt) Exec(args []driver.Value) (driver.Result, error) {
- return stmt.ExecContext(context.Background(), toNamedValues(args))
-}
-
-func (stmt *PostgreSQLStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
- res, err := stmt.stmt.ExecContext(ctx, args)
- err = processPostgresError(err)
- return res, err
-}
-
-func (stmt *PostgreSQLStmt) Query(args []driver.Value) (driver.Rows, error) {
- return stmt.QueryContext(context.Background(), toNamedValues(args))
-}
-
-func (stmt *PostgreSQLStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
- rows, err := stmt.stmt.QueryContext(ctx, args)
- err = processPostgresError(err)
- return rows, err
-}
-
-// SQLiteDriver is our own wrapper around the
-// sqlite.Driver{} type in order to wrap further
-// SQL driver types with our own functionality,
-// e.g. hooks, retries and err processing.
-type SQLiteDriver struct{}
-
-func (d *SQLiteDriver) Open(name string) (driver.Conn, error) {
- c, err := sqliteDriver.Open(name)
- if err != nil {
- return nil, err
- }
- return &SQLiteConn{conn: c.(conn)}, nil
-}
-
-type SQLiteConn struct{ conn }
-
-func (c *SQLiteConn) Begin() (driver.Tx, error) {
- return c.BeginTx(context.Background(), driver.TxOptions{})
-}
-
-func (c *SQLiteConn) BeginTx(ctx context.Context, opts driver.TxOptions) (tx driver.Tx, err error) {
- err = retryOnBusy(ctx, func() error {
- tx, err = c.conn.BeginTx(ctx, opts)
- err = processSQLiteError(err)
- return err
- })
- if err != nil {
- return nil, err
- }
- return &SQLiteTx{Context: ctx, Tx: tx}, nil
-}
-
-func (c *SQLiteConn) Prepare(query string) (driver.Stmt, error) {
- return c.PrepareContext(context.Background(), query)
-}
-
-func (c *SQLiteConn) PrepareContext(ctx context.Context, query string) (st driver.Stmt, err error) {
- err = retryOnBusy(ctx, func() error {
- st, err = c.conn.PrepareContext(ctx, query)
- err = processSQLiteError(err)
- return err
- })
- if err != nil {
- return nil, err
- }
- return &SQLiteStmt{st.(stmt)}, nil
-}
-
-func (c *SQLiteConn) Exec(query string, args []driver.Value) (driver.Result, error) {
- return c.ExecContext(context.Background(), query, toNamedValues(args))
-}
-
-func (c *SQLiteConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (result driver.Result, err error) {
- err = retryOnBusy(ctx, func() error {
- result, err = c.conn.ExecContext(ctx, query, args)
- err = processSQLiteError(err)
- return err
- })
- return
-}
-
-func (c *SQLiteConn) Query(query string, args []driver.Value) (driver.Rows, error) {
- return c.QueryContext(context.Background(), query, toNamedValues(args))
-}
-
-func (c *SQLiteConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (rows driver.Rows, err error) {
- err = retryOnBusy(ctx, func() error {
- rows, err = c.conn.QueryContext(ctx, query, args)
- err = processSQLiteError(err)
- return err
- })
- return
-}
-
-func (c *SQLiteConn) Close() error {
- // see: https://www.sqlite.org/pragma.html#pragma_optimize
- const onClose = "PRAGMA analysis_limit=1000; PRAGMA optimize;"
- _, _ = c.conn.ExecContext(context.Background(), onClose, nil)
- return c.conn.Close()
-}
-
-type SQLiteTx struct {
- context.Context
- driver.Tx
-}
-
-func (tx *SQLiteTx) Commit() (err error) {
- err = retryOnBusy(tx.Context, func() error {
- err = tx.Tx.Commit()
- err = processSQLiteError(err)
- return err
- })
- return
-}
-
-func (tx *SQLiteTx) Rollback() (err error) {
- err = retryOnBusy(tx.Context, func() error {
- err = tx.Tx.Rollback()
- err = processSQLiteError(err)
- return err
- })
- return
-}
-
-type SQLiteStmt struct{ stmt }
-
-func (stmt *SQLiteStmt) Exec(args []driver.Value) (driver.Result, error) {
- return stmt.ExecContext(context.Background(), toNamedValues(args))
-}
-
-func (stmt *SQLiteStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (res driver.Result, err error) {
- err = retryOnBusy(ctx, func() error {
- res, err = stmt.stmt.ExecContext(ctx, args)
- err = processSQLiteError(err)
- return err
- })
- return
-}
-
-func (stmt *SQLiteStmt) Query(args []driver.Value) (driver.Rows, error) {
- return stmt.QueryContext(context.Background(), toNamedValues(args))
-}
-
-func (stmt *SQLiteStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (rows driver.Rows, err error) {
- err = retryOnBusy(ctx, func() error {
- rows, err = stmt.stmt.QueryContext(ctx, args)
- err = processSQLiteError(err)
- return err
- })
- return
-}
-
-type conn interface {
- driver.Conn
- driver.ConnPrepareContext
- driver.ExecerContext
- driver.QueryerContext
- driver.ConnBeginTx
-}
-
-type stmt interface {
- driver.Stmt
- driver.StmtExecContext
- driver.StmtQueryContext
-}
-
-// retryOnBusy will retry given function on returned 'errBusy'.
-func retryOnBusy(ctx context.Context, fn func() error) error {
- if err := fn(); err != errBusy {
- return err
- }
- return retryOnBusySlow(ctx, fn)
-}
-
-// retryOnBusySlow is the outlined form of retryOnBusy, to allow the fast path (i.e. only
-// 1 attempt) to be inlined, leaving the slow retry loop to be a separate function call.
-func retryOnBusySlow(ctx context.Context, fn func() error) error {
- var backoff time.Duration
-
- for i := 0; ; i++ {
- // backoff according to a multiplier of 2ms * 2^2n,
- // up to a maximum possible backoff time of 5 minutes.
- //
- // this works out as the following:
- // 4ms
- // 16ms
- // 64ms
- // 256ms
- // 1.024s
- // 4.096s
- // 16.384s
- // 1m5.536s
- // 4m22.144s
- backoff = 2 * time.Millisecond * (1 << (2*i + 1))
- if backoff >= 5*time.Minute {
- break
- }
-
- select {
- // Context cancelled.
- case <-ctx.Done():
- return ctx.Err()
-
- // Backoff for some time.
- case <-time.After(backoff):
- }
-
- // Perform func.
- err := fn()
-
- if err != errBusy {
- // May be nil, or may be
- // some other error, either
- // way return here.
- return err
- }
- }
-
- return gtserror.Newf("%w (waited > %s)", db.ErrBusyTimeout, backoff)
-}
-
-// toNamedValues converts older driver.Value types to driver.NamedValue types.
-func toNamedValues(args []driver.Value) []driver.NamedValue {
- if args == nil {
- return nil
- }
- args2 := make([]driver.NamedValue, len(args))
- for i := range args {
- args2[i] = driver.NamedValue{
- Ordinal: i + 1,
- Value: args[i],
- }
- }
- return args2
+ // register our SQL driver implementations.
+ sql.Register("pgx-gts", &postgres.Driver{})
+ sql.Register("sqlite-gts", &sqlite.Driver{})
}
diff --git a/internal/db/bundb/errors.go b/internal/db/bundb/errors.go
deleted file mode 100644
index f2633786a..000000000
--- a/internal/db/bundb/errors.go
+++ /dev/null
@@ -1,105 +0,0 @@
-// 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/>.
-
-package bundb
-
-import (
- "database/sql/driver"
- "errors"
-
- "github.com/jackc/pgx/v5/pgconn"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "modernc.org/sqlite"
- sqlite3 "modernc.org/sqlite/lib"
-)
-
-// errBusy is a sentinel error indicating
-// busy database (e.g. retry needed).
-var errBusy = errors.New("busy")
-
-// processPostgresError processes an error, replacing any postgres specific errors with our own error type
-func processPostgresError(err error) error {
- // Catch nil errs.
- if err == nil {
- return nil
- }
-
- // Attempt to cast as postgres
- pgErr, ok := err.(*pgconn.PgError)
- if !ok {
- return err
- }
-
- // Handle supplied error code:
- // (https://www.postgresql.org/docs/10/errcodes-appendix.html)
- switch pgErr.Code { //nolint
- case "23505" /* unique_violation */ :
- return db.ErrAlreadyExists
- }
-
- return err
-}
-
-// processSQLiteError processes an error, replacing any sqlite specific errors with our own error type
-func processSQLiteError(err error) error {
- // Catch nil errs.
- if err == nil {
- return nil
- }
-
- // Attempt to cast as sqlite
- sqliteErr, ok := err.(*sqlite.Error)
- if !ok {
- return err
- }
-
- // Handle supplied error code:
- switch sqliteErr.Code() {
- case sqlite3.SQLITE_CONSTRAINT_UNIQUE,
- sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY:
- return db.ErrAlreadyExists
- case sqlite3.SQLITE_BUSY,
- sqlite3.SQLITE_BUSY_SNAPSHOT,
- sqlite3.SQLITE_BUSY_RECOVERY:
- return errBusy
- case sqlite3.SQLITE_BUSY_TIMEOUT:
- return db.ErrBusyTimeout
-
- // WORKAROUND:
- // text copied from matrix dev chat:
- //
- // okay i've found a workaround for now. so between
- // v1.29.0 and v1.29.2 (modernc.org/sqlite) is that
- // slightly tweaked interruptOnDone() behaviour, which
- // causes interrupt to (imo, correctly) get called when
- // a context is cancelled to cancel the running query. the
- // issue is that every single query after that point seems
- // to still then return interrupted. so as you thought,
- // maybe that query count isn't being decremented. i don't
- // think it's our code, but i haven't ruled it out yet.
- //
- // the workaround for now is adding to our sqlite error
- // processor to replace an SQLITE_INTERRUPTED code with
- // driver.ErrBadConn, which hints to the golang sql package
- // that the conn needs to be closed and a new one opened
- //
- case sqlite3.SQLITE_INTERRUPT:
- return driver.ErrBadConn
- }
-
- return err
-}
diff --git a/internal/db/bundb/tag_test.go b/internal/db/bundb/tag_test.go
index 324398d27..3647c92de 100644
--- a/internal/db/bundb/tag_test.go
+++ b/internal/db/bundb/tag_test.go
@@ -19,6 +19,7 @@ package bundb_test
import (
"context"
+ "errors"
"testing"
"github.com/stretchr/testify/suite"
@@ -82,10 +83,20 @@ func (suite *TagTestSuite) TestPutTag() {
// Subsequent inserts should fail
// since all these tags are equivalent.
- suite.ErrorIs(err, db.ErrAlreadyExists)
+ if !suite.ErrorIs(err, db.ErrAlreadyExists) {
+ suite.T().Logf("%T(%v) %v", err, err, unwrap(err))
+ }
}
}
func TestTagTestSuite(t *testing.T) {
suite.Run(t, new(TagTestSuite))
}
+
+func unwrap(err error) (errs []error) {
+ for err != nil {
+ errs = append(errs, err)
+ err = errors.Unwrap(err)
+ }
+ return
+}
diff --git a/internal/db/error.go b/internal/db/error.go
index b8e488297..43dd34df7 100644
--- a/internal/db/error.go
+++ b/internal/db/error.go
@@ -29,8 +29,4 @@ var (
// ErrAlreadyExists is returned when a conflict was encountered in the db when doing an insert.
ErrAlreadyExists = errors.New("already exists")
-
- // ErrBusyTimeout is returned if the database connection indicates the connection is too busy
- // to complete the supplied query. This is generally intended to be handled internally by the DB.
- ErrBusyTimeout = errors.New("busy timeout")
)
diff --git a/internal/db/postgres/driver.go b/internal/db/postgres/driver.go
new file mode 100644
index 000000000..994c9ffba
--- /dev/null
+++ b/internal/db/postgres/driver.go
@@ -0,0 +1,209 @@
+// 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/>.
+
+package postgres
+
+import (
+ "context"
+ "database/sql/driver"
+
+ pgx "github.com/jackc/pgx/v5/stdlib"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+)
+
+var (
+ // global PostgreSQL driver instances.
+ postgresDriver = pgx.GetDefaultDriver().(*pgx.Driver)
+
+ // check the postgres driver types
+ // conforms to our interface types.
+ // (note SQLite doesn't export their
+ // driver types, and gets checked in
+ // tests very regularly anywho).
+ _ connIface = (*pgx.Conn)(nil)
+ _ stmtIface = (*pgx.Stmt)(nil)
+ _ rowsIface = (*pgx.Rows)(nil)
+)
+
+// Driver is our own wrapper around the
+// pgx/stdlib.Driver{} type in order to wrap further
+// SQL driver types with our own err processing.
+type Driver struct{}
+
+func (d *Driver) Open(name string) (driver.Conn, error) {
+ conn, err := postgresDriver.Open(name)
+ if err != nil {
+ err = processPostgresError(err)
+ return nil, err
+ }
+ return &postgresConn{conn.(connIface)}, nil
+}
+
+func (d *Driver) OpenConnector(name string) (driver.Connector, error) {
+ cc, err := postgresDriver.OpenConnector(name)
+ if err != nil {
+ err = processPostgresError(err)
+ return nil, err
+ }
+ return &postgresConnector{driver: d, Connector: cc}, nil
+}
+
+type postgresConnector struct {
+ driver *Driver
+ driver.Connector
+}
+
+func (c *postgresConnector) Driver() driver.Driver { return c.driver }
+
+func (c *postgresConnector) Connect(ctx context.Context) (driver.Conn, error) {
+ conn, err := c.Connector.Connect(ctx)
+ if err != nil {
+ err = processPostgresError(err)
+ return nil, err
+ }
+ return &postgresConn{conn.(connIface)}, nil
+}
+
+type postgresConn struct{ connIface }
+
+func (c *postgresConn) Begin() (driver.Tx, error) {
+ return c.BeginTx(context.Background(), driver.TxOptions{})
+}
+
+func (c *postgresConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
+ tx, err := c.connIface.BeginTx(ctx, opts)
+ err = processPostgresError(err)
+ if err != nil {
+ return nil, err
+ }
+ return &postgresTx{tx}, nil
+}
+
+func (c *postgresConn) Prepare(query string) (driver.Stmt, error) {
+ return c.PrepareContext(context.Background(), query)
+}
+
+func (c *postgresConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
+ st, err := c.connIface.PrepareContext(ctx, query)
+ err = processPostgresError(err)
+ if err != nil {
+ return nil, err
+ }
+ return &postgresStmt{st.(stmtIface)}, nil
+}
+
+func (c *postgresConn) Exec(query string, args []driver.Value) (driver.Result, error) {
+ return c.ExecContext(context.Background(), query, db.ToNamedValues(args))
+}
+
+func (c *postgresConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
+ result, err := c.connIface.ExecContext(ctx, query, args)
+ err = processPostgresError(err)
+ return result, err
+}
+
+func (c *postgresConn) Query(query string, args []driver.Value) (driver.Rows, error) {
+ return c.QueryContext(context.Background(), query, db.ToNamedValues(args))
+}
+
+func (c *postgresConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
+ rows, err := c.connIface.QueryContext(ctx, query, args)
+ err = processPostgresError(err)
+ if err != nil {
+ return nil, err
+ }
+ return &postgresRows{rows.(rowsIface)}, nil
+}
+
+func (c *postgresConn) Close() error {
+ err := c.connIface.Close()
+ return processPostgresError(err)
+}
+
+type postgresTx struct{ driver.Tx }
+
+func (tx *postgresTx) Commit() error {
+ err := tx.Tx.Commit()
+ return processPostgresError(err)
+}
+
+func (tx *postgresTx) Rollback() error {
+ err := tx.Tx.Rollback()
+ return processPostgresError(err)
+}
+
+type postgresStmt struct{ stmtIface }
+
+func (stmt *postgresStmt) Exec(args []driver.Value) (driver.Result, error) {
+ return stmt.ExecContext(context.Background(), db.ToNamedValues(args))
+}
+
+func (stmt *postgresStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
+ res, err := stmt.stmtIface.ExecContext(ctx, args)
+ err = processPostgresError(err)
+ return res, err
+}
+
+func (stmt *postgresStmt) Query(args []driver.Value) (driver.Rows, error) {
+ return stmt.QueryContext(context.Background(), db.ToNamedValues(args))
+}
+
+func (stmt *postgresStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
+ rows, err := stmt.stmtIface.QueryContext(ctx, args)
+ err = processPostgresError(err)
+ if err != nil {
+ return nil, err
+ }
+ return &postgresRows{rows.(rowsIface)}, nil
+}
+
+type postgresRows struct{ rowsIface }
+
+func (r *postgresRows) Next(dest []driver.Value) error {
+ err := r.rowsIface.Next(dest)
+ err = processPostgresError(err)
+ return err
+}
+
+func (r *postgresRows) Close() error {
+ err := r.rowsIface.Close()
+ err = processPostgresError(err)
+ return err
+}
+
+type connIface interface {
+ driver.Conn
+ driver.ConnPrepareContext
+ driver.ExecerContext
+ driver.QueryerContext
+ driver.ConnBeginTx
+}
+
+type stmtIface interface {
+ driver.Stmt
+ driver.StmtExecContext
+ driver.StmtQueryContext
+}
+
+type rowsIface interface {
+ driver.Rows
+ driver.RowsColumnTypeDatabaseTypeName
+ driver.RowsColumnTypeLength
+ driver.RowsColumnTypePrecisionScale
+ driver.RowsColumnTypeScanType
+ driver.RowsColumnTypeScanType
+}
diff --git a/internal/db/postgres/errors.go b/internal/db/postgres/errors.go
new file mode 100644
index 000000000..cb8989a73
--- /dev/null
+++ b/internal/db/postgres/errors.go
@@ -0,0 +1,46 @@
+// 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/>.
+
+package postgres
+
+import (
+ "fmt"
+
+ "github.com/jackc/pgx/v5/pgconn"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+)
+
+// processPostgresError processes an error, replacing any
+// postgres specific errors with our own error type
+func processPostgresError(err error) error {
+ // Attempt to cast as postgres
+ pgErr, ok := err.(*pgconn.PgError)
+ if !ok {
+ return err
+ }
+
+ // Handle supplied error code:
+ // (https://www.postgresql.org/docs/10/errcodes-appendix.html)
+ switch pgErr.Code { //nolint
+ case "23505" /* unique_violation */ :
+ return db.ErrAlreadyExists
+ }
+
+ // Wrap the returned error with the code and
+ // extended code for easier debugging later.
+ return fmt.Errorf("%w (code=%s)", err, pgErr.Code)
+}
diff --git a/internal/db/sqlite/driver.go b/internal/db/sqlite/driver.go
new file mode 100644
index 000000000..11cb6b27d
--- /dev/null
+++ b/internal/db/sqlite/driver.go
@@ -0,0 +1,197 @@
+// 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/>.
+
+//go:build !wasmsqlite3
+
+package sqlite
+
+import (
+ "context"
+ "database/sql/driver"
+
+ "modernc.org/sqlite"
+
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+)
+
+// Driver is our own wrapper around the
+// sqlite.Driver{} type in order to wrap
+// further SQL types with our own
+// functionality, e.g. err processing.
+type Driver struct{ sqlite.Driver }
+
+func (d *Driver) Open(name string) (driver.Conn, error) {
+ conn, err := d.Driver.Open(name)
+ if err != nil {
+ err = processSQLiteError(err)
+ return nil, err
+ }
+ return &sqliteConn{conn.(connIface)}, nil
+}
+
+type sqliteConn struct{ connIface }
+
+func (c *sqliteConn) Begin() (driver.Tx, error) {
+ return c.BeginTx(context.Background(), driver.TxOptions{})
+}
+
+func (c *sqliteConn) BeginTx(ctx context.Context, opts driver.TxOptions) (tx driver.Tx, err error) {
+ tx, err = c.connIface.BeginTx(ctx, opts)
+ err = processSQLiteError(err)
+ if err != nil {
+ return nil, err
+ }
+ return &sqliteTx{tx}, nil
+}
+
+func (c *sqliteConn) Prepare(query string) (driver.Stmt, error) {
+ return c.PrepareContext(context.Background(), query)
+}
+
+func (c *sqliteConn) PrepareContext(ctx context.Context, query string) (stmt driver.Stmt, err error) {
+ stmt, err = c.connIface.PrepareContext(ctx, query)
+ err = processSQLiteError(err)
+ if err != nil {
+ return nil, err
+ }
+ return &sqliteStmt{stmtIface: stmt.(stmtIface)}, nil
+}
+
+func (c *sqliteConn) Exec(query string, args []driver.Value) (driver.Result, error) {
+ return c.ExecContext(context.Background(), query, db.ToNamedValues(args))
+}
+
+func (c *sqliteConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (res driver.Result, err error) {
+ res, err = c.connIface.ExecContext(ctx, query, args)
+ err = processSQLiteError(err)
+ return
+}
+
+func (c *sqliteConn) Query(query string, args []driver.Value) (driver.Rows, error) {
+ return c.QueryContext(context.Background(), query, db.ToNamedValues(args))
+}
+
+func (c *sqliteConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (rows driver.Rows, err error) {
+ rows, err = c.connIface.QueryContext(ctx, query, args)
+ err = processSQLiteError(err)
+ if err != nil {
+ return nil, err
+ }
+ return &sqliteRows{rows.(rowsIface)}, nil
+}
+
+func (c *sqliteConn) Close() (err error) {
+ // see: https://www.sqlite.org/pragma.html#pragma_optimize
+ const onClose = "PRAGMA analysis_limit=1000; PRAGMA optimize;"
+ _, _ = c.connIface.ExecContext(context.Background(), onClose, nil)
+
+ // Finally, close the conn.
+ err = c.connIface.Close()
+ return
+}
+
+type sqliteTx struct{ driver.Tx }
+
+func (tx *sqliteTx) Commit() (err error) {
+ err = tx.Tx.Commit()
+ err = processSQLiteError(err)
+ return
+}
+
+func (tx *sqliteTx) Rollback() (err error) {
+ err = tx.Tx.Rollback()
+ err = processSQLiteError(err)
+ return
+}
+
+type sqliteStmt struct{ stmtIface }
+
+func (stmt *sqliteStmt) Exec(args []driver.Value) (driver.Result, error) {
+ return stmt.ExecContext(context.Background(), db.ToNamedValues(args))
+}
+
+func (stmt *sqliteStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (res driver.Result, err error) {
+ res, err = stmt.stmtIface.ExecContext(ctx, args)
+ err = processSQLiteError(err)
+ return
+}
+
+func (stmt *sqliteStmt) Query(args []driver.Value) (driver.Rows, error) {
+ return stmt.QueryContext(context.Background(), db.ToNamedValues(args))
+}
+
+func (stmt *sqliteStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (rows driver.Rows, err error) {
+ rows, err = stmt.stmtIface.QueryContext(ctx, args)
+ err = processSQLiteError(err)
+ if err != nil {
+ return nil, err
+ }
+ return &sqliteRows{rows.(rowsIface)}, nil
+}
+
+func (stmt *sqliteStmt) Close() (err error) {
+ err = stmt.stmtIface.Close()
+ err = processSQLiteError(err)
+ return
+}
+
+type sqliteRows struct{ rowsIface }
+
+func (r *sqliteRows) Next(dest []driver.Value) (err error) {
+ err = r.rowsIface.Next(dest)
+ err = processSQLiteError(err)
+ return
+}
+
+func (r *sqliteRows) Close() (err error) {
+ err = r.rowsIface.Close()
+ err = processSQLiteError(err)
+ return
+}
+
+// connIface is the driver.Conn interface
+// types (and the like) that modernc.org/sqlite.conn
+// conforms to. Useful so you don't need
+// to repeatedly perform checks yourself.
+type connIface interface {
+ driver.Conn
+ driver.ConnBeginTx
+ driver.ConnPrepareContext
+ driver.ExecerContext
+ driver.QueryerContext
+}
+
+// StmtIface is the driver.Stmt interface
+// types (and the like) that modernc.org/sqlite.stmt
+// conforms to. Useful so you don't need
+// to repeatedly perform checks yourself.
+type stmtIface interface {
+ driver.Stmt
+ driver.StmtExecContext
+ driver.StmtQueryContext
+}
+
+// RowsIface is the driver.Rows interface
+// types (and the like) that modernc.org/sqlite.rows
+// conforms to. Useful so you don't need
+// to repeatedly perform checks yourself.
+type rowsIface interface {
+ driver.Rows
+ driver.RowsColumnTypeDatabaseTypeName
+ driver.RowsColumnTypeLength
+ driver.RowsColumnTypeScanType
+}
diff --git a/internal/db/sqlite/driver_wasmsqlite3.go b/internal/db/sqlite/driver_wasmsqlite3.go
new file mode 100644
index 000000000..afe499a98
--- /dev/null
+++ b/internal/db/sqlite/driver_wasmsqlite3.go
@@ -0,0 +1,211 @@
+// 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/>.
+
+//go:build wasmsqlite3
+
+package sqlite
+
+import (
+ "context"
+ "database/sql/driver"
+
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+
+ "github.com/ncruces/go-sqlite3"
+ sqlite3driver "github.com/ncruces/go-sqlite3/driver"
+ _ "github.com/ncruces/go-sqlite3/embed" // embed wasm binary
+ _ "github.com/ncruces/go-sqlite3/vfs/memdb" // include memdb vfs
+)
+
+// Driver is our own wrapper around the
+// driver.SQLite{} type in order to wrap
+// further SQL types with our own
+// functionality, e.g. err processing.
+type Driver struct{ sqlite3driver.SQLite }
+
+func (d *Driver) Open(name string) (driver.Conn, error) {
+ conn, err := d.SQLite.Open(name)
+ if err != nil {
+ err = processSQLiteError(err)
+ return nil, err
+ }
+ return &sqliteConn{conn.(connIface)}, nil
+}
+
+func (d *Driver) OpenConnector(name string) (driver.Connector, error) {
+ cc, err := d.SQLite.OpenConnector(name)
+ if err != nil {
+ return nil, err
+ }
+ return &sqliteConnector{driver: d, Connector: cc}, nil
+}
+
+type sqliteConnector struct {
+ driver *Driver
+ driver.Connector
+}
+
+func (c *sqliteConnector) Driver() driver.Driver { return c.driver }
+
+func (c *sqliteConnector) Connect(ctx context.Context) (driver.Conn, error) {
+ conn, err := c.Connector.Connect(ctx)
+ err = processSQLiteError(err)
+ if err != nil {
+ return nil, err
+ }
+ return &sqliteConn{conn.(connIface)}, nil
+}
+
+type sqliteConn struct{ connIface }
+
+func (c *sqliteConn) Begin() (driver.Tx, error) {
+ return c.BeginTx(context.Background(), driver.TxOptions{})
+}
+
+func (c *sqliteConn) BeginTx(ctx context.Context, opts driver.TxOptions) (tx driver.Tx, err error) {
+ tx, err = c.connIface.BeginTx(ctx, opts)
+ err = processSQLiteError(err)
+ if err != nil {
+ return nil, err
+ }
+ return &sqliteTx{tx}, nil
+}
+
+func (c *sqliteConn) Prepare(query string) (driver.Stmt, error) {
+ return c.PrepareContext(context.Background(), query)
+}
+
+func (c *sqliteConn) PrepareContext(ctx context.Context, query string) (stmt driver.Stmt, err error) {
+ stmt, err = c.connIface.PrepareContext(ctx, query)
+ err = processSQLiteError(err)
+ if err != nil {
+ return nil, err
+ }
+ return &sqliteStmt{stmtIface: stmt.(stmtIface)}, nil
+}
+
+func (c *sqliteConn) Exec(query string, args []driver.Value) (driver.Result, error) {
+ return c.ExecContext(context.Background(), query, db.ToNamedValues(args))
+}
+
+func (c *sqliteConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (res driver.Result, err error) {
+ res, err = c.connIface.ExecContext(ctx, query, args)
+ err = processSQLiteError(err)
+ return
+}
+
+func (c *sqliteConn) Close() (err error) {
+ // Get acces the underlying raw sqlite3 conn.
+ raw := c.connIface.(sqlite3.DriverConn).Raw()
+
+ // see: https://www.sqlite.org/pragma.html#pragma_optimize
+ const onClose = "PRAGMA analysis_limit=1000; PRAGMA optimize;"
+ _ = raw.Exec(onClose)
+
+ // Finally, close.
+ err = raw.Close()
+ return
+}
+
+type sqliteTx struct{ driver.Tx }
+
+func (tx *sqliteTx) Commit() (err error) {
+ err = tx.Tx.Commit()
+ err = processSQLiteError(err)
+ return
+}
+
+func (tx *sqliteTx) Rollback() (err error) {
+ err = tx.Tx.Rollback()
+ err = processSQLiteError(err)
+ return
+}
+
+type sqliteStmt struct{ stmtIface }
+
+func (stmt *sqliteStmt) Exec(args []driver.Value) (driver.Result, error) {
+ return stmt.ExecContext(context.Background(), db.ToNamedValues(args))
+}
+
+func (stmt *sqliteStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (res driver.Result, err error) {
+ res, err = stmt.stmtIface.ExecContext(ctx, args)
+ err = processSQLiteError(err)
+ return
+}
+
+func (stmt *sqliteStmt) Query(args []driver.Value) (driver.Rows, error) {
+ return stmt.QueryContext(context.Background(), db.ToNamedValues(args))
+}
+
+func (stmt *sqliteStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (rows driver.Rows, err error) {
+ rows, err = stmt.stmtIface.QueryContext(ctx, args)
+ err = processSQLiteError(err)
+ if err != nil {
+ return nil, err
+ }
+ return &sqliteRows{rows.(rowsIface)}, nil
+}
+
+func (stmt *sqliteStmt) Close() (err error) {
+ err = stmt.stmtIface.Close()
+ err = processSQLiteError(err)
+ return
+}
+
+type sqliteRows struct{ rowsIface }
+
+func (r *sqliteRows) Next(dest []driver.Value) (err error) {
+ err = r.rowsIface.Next(dest)
+ err = processSQLiteError(err)
+ return
+}
+
+func (r *sqliteRows) Close() (err error) {
+ err = r.rowsIface.Close()
+ err = processSQLiteError(err)
+ return
+}
+
+// connIface is the driver.Conn interface
+// types (and the like) that go-sqlite3/driver.conn
+// conforms to. Useful so you don't need
+// to repeatedly perform checks yourself.
+type connIface interface {
+ driver.Conn
+ driver.ConnBeginTx
+ driver.ConnPrepareContext
+ driver.ExecerContext
+}
+
+// StmtIface is the driver.Stmt interface
+// types (and the like) that go-sqlite3/driver.stmt
+// conforms to. Useful so you don't need
+// to repeatedly perform checks yourself.
+type stmtIface interface {
+ driver.Stmt
+ driver.StmtExecContext
+ driver.StmtQueryContext
+}
+
+// RowsIface is the driver.Rows interface
+// types (and the like) that go-sqlite3/driver.rows
+// conforms to. Useful so you don't need
+// to repeatedly perform checks yourself.
+type rowsIface interface {
+ driver.Rows
+ driver.RowsColumnTypeDatabaseTypeName
+}
diff --git a/internal/db/sqlite/errors.go b/internal/db/sqlite/errors.go
new file mode 100644
index 000000000..b07b026de
--- /dev/null
+++ b/internal/db/sqlite/errors.go
@@ -0,0 +1,62 @@
+// 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/>.
+
+//go:build !wasmsqlite3
+
+package sqlite
+
+import (
+ "database/sql/driver"
+ "fmt"
+
+ "modernc.org/sqlite"
+ sqlite3 "modernc.org/sqlite/lib"
+
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+)
+
+// processSQLiteError processes an sqlite3.Error to
+// handle conversion to any of our common db types.
+func processSQLiteError(err error) error {
+ // Attempt to cast as sqlite error.
+ sqliteErr, ok := err.(*sqlite.Error)
+ if !ok {
+ return err
+ }
+
+ // Handle supplied error code:
+ switch sqliteErr.Code() {
+ case sqlite3.SQLITE_CONSTRAINT_UNIQUE,
+ sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY:
+ return db.ErrAlreadyExists
+
+ // Busy should be very rare, but
+ // on busy tell the database to close
+ // the connection, re-open and re-attempt
+ // which should give a necessary timeout.
+ case sqlite3.SQLITE_BUSY,
+ sqlite3.SQLITE_BUSY_RECOVERY,
+ sqlite3.SQLITE_BUSY_SNAPSHOT:
+ return driver.ErrBadConn
+ }
+
+ // Wrap the returned error with the code and
+ // extended code for easier debugging later.
+ return fmt.Errorf("%w (code=%d)", err,
+ sqliteErr.Code(),
+ )
+}
diff --git a/internal/db/sqlite/errors_wasmsqlite3.go b/internal/db/sqlite/errors_wasmsqlite3.go
new file mode 100644
index 000000000..26668a898
--- /dev/null
+++ b/internal/db/sqlite/errors_wasmsqlite3.go
@@ -0,0 +1,60 @@
+// 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/>.
+
+//go:build wasmsqlite3
+
+package sqlite
+
+import (
+ "database/sql/driver"
+ "fmt"
+
+ "github.com/ncruces/go-sqlite3"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+)
+
+// processSQLiteError processes an sqlite3.Error to
+// handle conversion to any of our common db types.
+func processSQLiteError(err error) error {
+ // Attempt to cast as sqlite error.
+ sqliteErr, ok := err.(*sqlite3.Error)
+ if !ok {
+ return err
+ }
+
+ // Handle supplied error code:
+ switch sqliteErr.ExtendedCode() {
+ case sqlite3.CONSTRAINT_UNIQUE,
+ sqlite3.CONSTRAINT_PRIMARYKEY:
+ return db.ErrAlreadyExists
+
+ // Busy should be very rare, but on
+ // busy tell the database to close the
+ // connection, re-open and re-attempt
+ // which should give necessary timeout.
+ case sqlite3.BUSY_RECOVERY,
+ sqlite3.BUSY_SNAPSHOT:
+ return driver.ErrBadConn
+ }
+
+ // Wrap the returned error with the code and
+ // extended code for easier debugging later.
+ return fmt.Errorf("%w (code=%d extended=%d)", err,
+ sqliteErr.Code(),
+ sqliteErr.ExtendedCode(),
+ )
+}
diff --git a/internal/db/util.go b/internal/db/util.go
new file mode 100644
index 000000000..9cd29f2fc
--- /dev/null
+++ b/internal/db/util.go
@@ -0,0 +1,35 @@
+// 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/>.
+
+package db
+
+import "database/sql/driver"
+
+// ToNamedValues converts older driver.Value types to driver.NamedValue types.
+func ToNamedValues(args []driver.Value) []driver.NamedValue {
+ if args == nil {
+ return nil
+ }
+ args2 := make([]driver.NamedValue, len(args))
+ for i := range args {
+ args2[i] = driver.NamedValue{
+ Ordinal: i + 1,
+ Value: args[i],
+ }
+ }
+ return args2
+}