check / check (push) Waiting to run
WithReadTx ended the process when its rollback failed, including after Ctrl-C, when database/sql has already rolled the transaction back and Rollback returns sql.ErrTxDone. It now handles the rollback as WithTx does. The query functions ended the process when closing their rows failed; they now ignore the deferred close, because database/sql reports a close failure through rows.Err(), which each returns, once the rows are read to the end. The unused database.Fatalf is removed. cleanupBlobTempFile returns its close or remove failure and the upload step returns it, so such a failure now fails the backup. No test makes a rows close fail: SQLite fails it only with the error the last read already returned. Model: opus-5-5
132 lines
3.6 KiB
Go
132 lines
3.6 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// Repositories provides access to all database repositories.
|
|
// It serves as a centralized access point for all database operations
|
|
// and manages transaction coordination across repositories.
|
|
type Repositories struct {
|
|
db *DB
|
|
Files *FileRepository
|
|
Chunks *ChunkRepository
|
|
Blobs *BlobRepository
|
|
FileChunks *FileChunkRepository
|
|
BlobChunks *BlobChunkRepository
|
|
ChunkFiles *ChunkFileRepository
|
|
Snapshots *SnapshotRepository
|
|
Uploads *UploadRepository
|
|
LocalMeta *LocalMetaRepository
|
|
}
|
|
|
|
// NewRepositories creates a new Repositories instance with all repository types.
|
|
// Each repository shares the same database connection for coordinated transactions.
|
|
func NewRepositories(db *DB) *Repositories {
|
|
return &Repositories{
|
|
db: db,
|
|
Files: NewFileRepository(db),
|
|
Chunks: NewChunkRepository(db),
|
|
Blobs: NewBlobRepository(db),
|
|
FileChunks: NewFileChunkRepository(db),
|
|
BlobChunks: NewBlobChunkRepository(db),
|
|
ChunkFiles: NewChunkFileRepository(db),
|
|
Snapshots: NewSnapshotRepository(db),
|
|
Uploads: NewUploadRepository(db.conn),
|
|
LocalMeta: NewLocalMetaRepository(db),
|
|
}
|
|
}
|
|
|
|
// TxFunc is a function that executes within a database transaction.
|
|
// The transaction is automatically committed if the function returns nil,
|
|
// or rolled back if it returns an error.
|
|
type TxFunc func(ctx context.Context, tx *sql.Tx) error
|
|
|
|
// WithTx executes a function within a write transaction.
|
|
// SQLite handles its own locking internally, so no explicit locking is needed.
|
|
// The transaction is automatically committed on success or rolled back on error.
|
|
// This method should be used for all write operations to ensure atomicity.
|
|
func (r *Repositories) WithTx(ctx context.Context, fn TxFunc) error {
|
|
LogSQL("WithTx", "Beginning transaction", "")
|
|
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("beginning transaction: %w", err)
|
|
}
|
|
|
|
LogSQL("WithTx", "Transaction started", "")
|
|
|
|
defer func() {
|
|
if p := recover(); p != nil {
|
|
// The panic is the failure to report, so a rollback error
|
|
// is dropped.
|
|
_ = tx.Rollback()
|
|
|
|
panic(p)
|
|
}
|
|
}()
|
|
|
|
err = fn(ctx, tx)
|
|
if err != nil {
|
|
// Once ctx is cancelled, database/sql rolls the transaction
|
|
// back itself and Rollback returns sql.ErrTxDone.
|
|
rollbackErr := tx.Rollback()
|
|
if rollbackErr != nil && !errors.Is(rollbackErr, sql.ErrTxDone) {
|
|
return fmt.Errorf("%w; rolling back transaction: %w",
|
|
err, rollbackErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
// DB returns the underlying database for direct queries
|
|
func (r *Repositories) DB() *DB {
|
|
return r.db
|
|
}
|
|
|
|
// WithReadTx executes a function within a read-only transaction.
|
|
// Read transactions can run concurrently with other read transactions
|
|
// but will be blocked by write transactions. The transaction is
|
|
// automatically committed on success or rolled back on error.
|
|
func (r *Repositories) WithReadTx(ctx context.Context, fn TxFunc) error {
|
|
opts := &sql.TxOptions{
|
|
ReadOnly: true,
|
|
}
|
|
|
|
tx, err := r.db.BeginTx(ctx, opts)
|
|
if err != nil {
|
|
return fmt.Errorf("beginning read transaction: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
if p := recover(); p != nil {
|
|
// The panic is the failure to report, so a rollback error
|
|
// is dropped.
|
|
_ = tx.Rollback()
|
|
|
|
panic(p)
|
|
}
|
|
}()
|
|
|
|
err = fn(ctx, tx)
|
|
if err != nil {
|
|
// Once ctx is cancelled, database/sql rolls the transaction
|
|
// back itself and Rollback returns sql.ErrTxDone.
|
|
rollbackErr := tx.Rollback()
|
|
if rollbackErr != nil && !errors.Is(rollbackErr, sql.ErrTxDone) {
|
|
return fmt.Errorf("%w; rolling back transaction: %w",
|
|
err, rollbackErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|