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