From 1e7b32490dfdccddd04f46d4b0416b48d749d51b Mon Sep 17 00:00:00 2001
From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com>
Date: Mon, 27 May 2024 15:46:15 +0000
Subject: [experiment] add alternative wasm sqlite3 implementation available
 via build-tag (#2863)
This allows for building GoToSocial with [SQLite transpiled to WASM](https://github.com/ncruces/go-sqlite3) and accessed through [Wazero](https://wazero.io/).
---
 internal/api/client/media/mediacreate_test.go |  28 +--
 internal/api/client/media/mediaupdate_test.go |  27 +-
 internal/api/fileserver/fileserver_test.go    |   6 +-
 internal/config/global.go                     |   6 +
 internal/config/state.go                      |  48 ++--
 internal/db/bundb/admin_test.go               |   1 +
 internal/db/bundb/bundb.go                    |  25 +-
 internal/db/bundb/drivers.go                  | 346 +-------------------------
 internal/db/bundb/errors.go                   | 105 --------
 internal/db/bundb/tag_test.go                 |  13 +-
 internal/db/error.go                          |   4 -
 internal/db/postgres/driver.go                | 209 ++++++++++++++++
 internal/db/postgres/errors.go                |  46 ++++
 internal/db/sqlite/driver.go                  | 197 +++++++++++++++
 internal/db/sqlite/driver_wasmsqlite3.go      | 211 ++++++++++++++++
 internal/db/sqlite/errors.go                  |  62 +++++
 internal/db/sqlite/errors_wasmsqlite3.go      |  60 +++++
 internal/db/util.go                           |  35 +++
 internal/gtserror/multi_test.go               |   4 -
 internal/util/paging_test.go                  |   5 +
 20 files changed, 907 insertions(+), 531 deletions(-)
 delete mode 100644 internal/db/bundb/errors.go
 create mode 100644 internal/db/postgres/driver.go
 create mode 100644 internal/db/postgres/errors.go
 create mode 100644 internal/db/sqlite/driver.go
 create mode 100644 internal/db/sqlite/driver_wasmsqlite3.go
 create mode 100644 internal/db/sqlite/errors.go
 create mode 100644 internal/db/sqlite/errors_wasmsqlite3.go
 create mode 100644 internal/db/util.go
(limited to 'internal')
diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go
index 00f385032..c2871aff0 100644
--- a/internal/api/client/media/mediacreate_test.go
+++ b/internal/api/client/media/mediacreate_test.go
@@ -38,7 +38,6 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/federation"
 	"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/media"
 	"github.com/superseriousbusiness/gotosocial/internal/oauth"
 	"github.com/superseriousbusiness/gotosocial/internal/processing"
@@ -77,19 +76,22 @@ type MediaCreateTestSuite struct {
 	TEST INFRASTRUCTURE
 */
 
-func (suite *MediaCreateTestSuite) SetupSuite() {
-	suite.state.Caches.Init()
+func (suite *MediaCreateTestSuite) SetupTest() {
 	testrig.StartNoopWorkers(&suite.state)
 
 	// setup standard items
 	testrig.InitTestConfig()
 	testrig.InitTestLog()
 
-	suite.db = testrig.NewTestDB(&suite.state)
-	suite.state.DB = suite.db
+	suite.state.Caches.Init()
+
 	suite.storage = testrig.NewInMemoryStorage()
 	suite.state.Storage = suite.storage
 
+	suite.db = testrig.NewTestDB(&suite.state)
+	testrig.StandardDBSetup(suite.db, nil)
+	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
+
 	suite.tc = typeutils.NewConverter(&suite.state)
 
 	testrig.StartTimelines(
@@ -106,21 +108,8 @@ func (suite *MediaCreateTestSuite) SetupSuite() {
 
 	// setup module being tested
 	suite.mediaModule = mediamodule.New(suite.processor)
-}
-
-func (suite *MediaCreateTestSuite) TearDownSuite() {
-	if err := suite.db.Close(); err != nil {
-		log.Panicf(nil, "error closing db connection: %s", err)
-	}
-	testrig.StopWorkers(&suite.state)
-}
-
-func (suite *MediaCreateTestSuite) SetupTest() {
-	suite.state.Caches.Init()
-
-	testrig.StandardDBSetup(suite.db, nil)
-	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
 
+	// setup test data
 	suite.testTokens = testrig.NewTestTokens()
 	suite.testClients = testrig.NewTestClients()
 	suite.testApplications = testrig.NewTestApplications()
@@ -132,6 +121,7 @@ func (suite *MediaCreateTestSuite) SetupTest() {
 func (suite *MediaCreateTestSuite) TearDownTest() {
 	testrig.StandardDBTeardown(suite.db)
 	testrig.StandardStorageTeardown(suite.storage)
+	testrig.StopWorkers(&suite.state)
 }
 
 /*
diff --git a/internal/api/client/media/mediaupdate_test.go b/internal/api/client/media/mediaupdate_test.go
index bb4e0f4ad..bb260ae4d 100644
--- a/internal/api/client/media/mediaupdate_test.go
+++ b/internal/api/client/media/mediaupdate_test.go
@@ -36,7 +36,6 @@ import (
 	"github.com/superseriousbusiness/gotosocial/internal/federation"
 	"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-	"github.com/superseriousbusiness/gotosocial/internal/log"
 	"github.com/superseriousbusiness/gotosocial/internal/media"
 	"github.com/superseriousbusiness/gotosocial/internal/oauth"
 	"github.com/superseriousbusiness/gotosocial/internal/processing"
@@ -75,18 +74,22 @@ type MediaUpdateTestSuite struct {
 	TEST INFRASTRUCTURE
 */
 
-func (suite *MediaUpdateTestSuite) SetupSuite() {
+func (suite *MediaUpdateTestSuite) SetupTest() {
 	testrig.StartNoopWorkers(&suite.state)
 
 	// setup standard items
 	testrig.InitTestConfig()
 	testrig.InitTestLog()
 
-	suite.db = testrig.NewTestDB(&suite.state)
-	suite.state.DB = suite.db
+	suite.state.Caches.Init()
+
 	suite.storage = testrig.NewInMemoryStorage()
 	suite.state.Storage = suite.storage
 
+	suite.db = testrig.NewTestDB(&suite.state)
+	testrig.StandardDBSetup(suite.db, nil)
+	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
+
 	suite.tc = typeutils.NewConverter(&suite.state)
 
 	testrig.StartTimelines(
@@ -103,21 +106,8 @@ func (suite *MediaUpdateTestSuite) SetupSuite() {
 
 	// setup module being tested
 	suite.mediaModule = mediamodule.New(suite.processor)
-}
-
-func (suite *MediaUpdateTestSuite) TearDownSuite() {
-	if err := suite.db.Close(); err != nil {
-		log.Panicf(nil, "error closing db connection: %s", err)
-	}
-	testrig.StopWorkers(&suite.state)
-}
-
-func (suite *MediaUpdateTestSuite) SetupTest() {
-	suite.state.Caches.Init()
-
-	testrig.StandardDBSetup(suite.db, nil)
-	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
 
+	// setup test data
 	suite.testTokens = testrig.NewTestTokens()
 	suite.testClients = testrig.NewTestClients()
 	suite.testApplications = testrig.NewTestApplications()
@@ -129,6 +119,7 @@ func (suite *MediaUpdateTestSuite) SetupTest() {
 func (suite *MediaUpdateTestSuite) TearDownTest() {
 	testrig.StandardDBTeardown(suite.db)
 	testrig.StandardStorageTeardown(suite.storage)
+	testrig.StopWorkers(&suite.state)
 }
 
 /*
diff --git a/internal/api/fileserver/fileserver_test.go b/internal/api/fileserver/fileserver_test.go
index b58433b9f..e5f684d0c 100644
--- a/internal/api/fileserver/fileserver_test.go
+++ b/internal/api/fileserver/fileserver_test.go
@@ -70,8 +70,6 @@ func (suite *FileserverTestSuite) SetupSuite() {
 	testrig.InitTestConfig()
 	testrig.InitTestLog()
 
-	suite.db = testrig.NewTestDB(&suite.state)
-	suite.state.DB = suite.db
 	suite.storage = testrig.NewInMemoryStorage()
 	suite.state.Storage = suite.storage
 
@@ -98,8 +96,12 @@ func (suite *FileserverTestSuite) SetupTest() {
 	suite.state.Caches.Init()
 	testrig.StartNoopWorkers(&suite.state)
 
+	suite.db = testrig.NewTestDB(&suite.state)
+	suite.state.DB = suite.db
+
 	testrig.StandardDBSetup(suite.db, nil)
 	testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
+
 	suite.testTokens = testrig.NewTestTokens()
 	suite.testClients = testrig.NewTestClients()
 	suite.testApplications = testrig.NewTestApplications()
diff --git a/internal/config/global.go b/internal/config/global.go
index 4bc5ac3d2..57af89d05 100644
--- a/internal/config/global.go
+++ b/internal/config/global.go
@@ -52,3 +52,9 @@ func LoadEarlyFlags(cmd *cobra.Command) error {
 func BindFlags(cmd *cobra.Command) error {
 	return global.BindFlags(cmd)
 }
+
+// Reset will totally clear global
+// ConfigState{}, loading defaults.
+func Reset() {
+	global.Reset()
+}
diff --git a/internal/config/state.go b/internal/config/state.go
index c55f7b2ec..d01e853a5 100644
--- a/internal/config/state.go
+++ b/internal/config/state.go
@@ -37,25 +37,9 @@ type ConfigState struct { //nolint
 
 // NewState returns a new initialized ConfigState instance.
 func NewState() *ConfigState {
-	viper := viper.New()
-
-	// Flag 'some-flag-name' becomes env var 'GTS_SOME_FLAG_NAME'
-	viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
-	viper.SetEnvPrefix("gts")
-
-	// Load appropriate named vals from env
-	viper.AutomaticEnv()
-
-	// Create new ConfigState with defaults
-	state := &ConfigState{
-		viper:  viper,
-		config: Defaults,
-	}
-
-	// Perform initial load into viper
-	state.reloadToViper()
-
-	return state
+	st := new(ConfigState)
+	st.Reset()
+	return st
 }
 
 // Config provides safe access to the ConfigState's contained Configuration,
@@ -116,6 +100,32 @@ func (st *ConfigState) Reload() (err error) {
 	return
 }
 
+// Reset will totally clear
+// ConfigState{}, loading defaults.
+func (st *ConfigState) Reset() {
+	// Do within lock.
+	st.mutex.Lock()
+	defer st.mutex.Unlock()
+
+	// Create new viper.
+	viper := viper.New()
+
+	// Flag 'some-flag-name' becomes env var 'GTS_SOME_FLAG_NAME'
+	viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
+	viper.SetEnvPrefix("gts")
+
+	// Load appropriate
+	// named vals from env.
+	viper.AutomaticEnv()
+
+	// Reset variables.
+	st.viper = viper
+	st.config = Defaults
+
+	// Load into viper.
+	st.reloadToViper()
+}
+
 // reloadToViper will reload Configuration{} values into viper.
 func (st *ConfigState) reloadToViper() {
 	raw, err := st.config.MarshalMap()
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 .
-
-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 .
+
+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 .
+
+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 .
+
+//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 .
+
+//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 .
+
+//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 .
+
+//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 .
+
+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
+}
diff --git a/internal/gtserror/multi_test.go b/internal/gtserror/multi_test.go
index 10c342415..b58c1b881 100644
--- a/internal/gtserror/multi_test.go
+++ b/internal/gtserror/multi_test.go
@@ -43,10 +43,6 @@ func TestMultiError(t *testing.T) {
 		t.Error("should be db.ErrAlreadyExists")
 	}
 
-	if errors.Is(err, db.ErrBusyTimeout) {
-		t.Error("should not be db.ErrBusyTimeout")
-	}
-
 	errString := err.Error()
 	expected := `sql: no rows in result set
 oopsie woopsie we did a fucky wucky etc
diff --git a/internal/util/paging_test.go b/internal/util/paging_test.go
index 66c5b2c56..bab7dcb7b 100644
--- a/internal/util/paging_test.go
+++ b/internal/util/paging_test.go
@@ -30,6 +30,7 @@ type PagingSuite struct {
 }
 
 func (suite *PagingSuite) TestPagingStandard() {
+	config.SetProtocol("https")
 	config.SetHost("example.org")
 
 	params := util.PageableResponseParams{
@@ -52,6 +53,7 @@ func (suite *PagingSuite) TestPagingStandard() {
 }
 
 func (suite *PagingSuite) TestPagingNoLimit() {
+	config.SetProtocol("https")
 	config.SetHost("example.org")
 
 	params := util.PageableResponseParams{
@@ -73,6 +75,7 @@ func (suite *PagingSuite) TestPagingNoLimit() {
 }
 
 func (suite *PagingSuite) TestPagingNoNextID() {
+	config.SetProtocol("https")
 	config.SetHost("example.org")
 
 	params := util.PageableResponseParams{
@@ -94,6 +97,7 @@ func (suite *PagingSuite) TestPagingNoNextID() {
 }
 
 func (suite *PagingSuite) TestPagingNoPrevID() {
+	config.SetProtocol("https")
 	config.SetHost("example.org")
 
 	params := util.PageableResponseParams{
@@ -115,6 +119,7 @@ func (suite *PagingSuite) TestPagingNoPrevID() {
 }
 
 func (suite *PagingSuite) TestPagingNoItems() {
+	config.SetProtocol("https")
 	config.SetHost("example.org")
 
 	params := util.PageableResponseParams{
-- 
cgit v1.2.3