[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/).
This commit is contained in:
kim 2024-05-27 15:46:15 +00:00 committed by GitHub
commit 1e7b32490d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
398 changed files with 86174 additions and 684 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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{})
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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")
)

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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(),
)
}

View file

@ -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(),
)
}

35
internal/db/util.go Normal file
View file

@ -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
}