From 5e62aaf724e7f3d79427aebd1d5cdbbb3547265e Mon Sep 17 00:00:00 2001 From: Terin Stock Date: Mon, 10 Mar 2025 00:11:39 +0100 Subject: [feat] utilize system's libsqlite3 Add support for using the system's libsqlite3 library by adding a driver using "github.com/mattn/go-sqlite3". --- internal/db/sqlite/driver.go | 2 +- internal/db/sqlite/driver_libsqlite3.go | 183 ++++++++++++++++++++++++++++++++ internal/db/sqlite/errors.go | 2 +- internal/db/sqlite/errors_libsqlite3.go | 60 +++++++++++ 4 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 internal/db/sqlite/driver_libsqlite3.go create mode 100644 internal/db/sqlite/errors_libsqlite3.go (limited to 'internal') diff --git a/internal/db/sqlite/driver.go b/internal/db/sqlite/driver.go index 203edaff3..e659d1ffa 100644 --- a/internal/db/sqlite/driver.go +++ b/internal/db/sqlite/driver.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//go:build !moderncsqlite3 && !nowasm +//go:build !moderncsqlite3 && !nowasm && !libsqlite3 package sqlite diff --git a/internal/db/sqlite/driver_libsqlite3.go b/internal/db/sqlite/driver_libsqlite3.go new file mode 100644 index 000000000..fe9df7aac --- /dev/null +++ b/internal/db/sqlite/driver_libsqlite3.go @@ -0,0 +1,183 @@ +// 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 !moderncsqlite3 && !nowasm && libsqlite3 + +package sqlite + +import ( + "context" + "database/sql/driver" + + sqlite3 "github.com/mattn/go-sqlite3" + "github.com/superseriousbusiness/gotosocial/internal/db" +) + +// 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{ sqlite3.SQLiteDriver } + +func (d *Driver) Open(name string) (driver.Conn, error) { + conn, err := d.SQLiteDriver.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) Close() (err error) { + // see: https://www.sqlite.org/pragma.html#pragma_optimize + const onClose = "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 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 + driver.RowsColumnTypeNullable +} diff --git a/internal/db/sqlite/errors.go b/internal/db/sqlite/errors.go index 9429a1bcd..784f9ef2d 100644 --- a/internal/db/sqlite/errors.go +++ b/internal/db/sqlite/errors.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//go:build !moderncsqlite3 && !nowasm +//go:build !moderncsqlite3 && !nowasm && !libsqlite3 package sqlite diff --git a/internal/db/sqlite/errors_libsqlite3.go b/internal/db/sqlite/errors_libsqlite3.go new file mode 100644 index 000000000..483d9ed14 --- /dev/null +++ b/internal/db/sqlite/errors_libsqlite3.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 !moderncsqlite3 && !nowasm && libsqlite3 + +package sqlite + +import ( + "database/sql/driver" + "fmt" + + sqlite3 "github.com/mattn/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.ErrConstraintUnique, + sqlite3.ErrConstraintPrimaryKey: + 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.ErrBusyRecovery, + sqlite3.ErrBusySnapshot: + 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, + ) +} -- cgit v1.2.3