Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Address low-frequency errors with concurrent use of `sqlitemigration`
([#99](https://github.com/zombiezen/go-sqlite/issues/99)).
- The error returned from `sqlitex.NewPool`
when trying to open an in-memory database
now gives correct advice
Expand Down
14 changes: 13 additions & 1 deletion sqlitemigration/sqlitemigration.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,19 @@ func rollback(conn *sqlite.Conn) {
}

func ensureAppID(conn *sqlite.Conn, wantAppID int32) (schemaVersion int32, err error) {
defer sqlitex.Save(conn)(&err)
// This transaction will later be upgraded to a write transaction. If at the point of upgrading
// to a write transaction, the database is locked, SQLite will fail immediately with
// SQLITE_BUSY and the busy timeout will have no effect, causing the pool to fail.
//
// If we use an immediate transaction, telling SQLite this is a write transaction, SQLite
// will attempt to lock the database immediately. If a lock cannot be acquired, the busy
// timeout is used allowing the transaction to wait until it can get a lock, thus allowing the
// pool to start successfully.
end, err := sqlitex.ImmediateTransaction(conn)
if err != nil {
return 0, err
}
defer end(&err)

var hasSchema bool
err = sqlitex.ExecuteTransient(conn, "VALUES ((SELECT COUNT(*) FROM sqlite_master) > 0);", &sqlitex.ExecOptions{
Expand Down
58 changes: 58 additions & 0 deletions sqlitemigration/sqlitemigration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"fmt"
"path/filepath"
"sync"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -917,6 +918,63 @@ func TestMigrate(t *testing.T) {
t.Error("Foreign keys were disabled after migration")
}
})

t.Run("Concurrent", func(t *testing.T) {
ctx := context.Background()
dbPath := filepath.Join(t.TempDir(), "concurrent.db")
schema := Schema{
AppID: 0xedbeef,
Migrations: []string{
`create table foo ( id integer primary key not null );`,
},
}

// Attempt to perform migrations while writing.
var wg sync.WaitGroup
defer wg.Wait()
const numConcurrent = 5
wg.Add(numConcurrent)
for i := 0; i < numConcurrent; i++ {
go func(i int) {
defer wg.Done()

conn, err := sqlite.OpenConn(dbPath, sqlite.OpenReadWrite, sqlite.OpenCreate)
if err != nil {
t.Error(err)
return
}
defer func() {
if err := conn.Close(); err != nil {
t.Error(err)
}
}()
if err := Migrate(ctx, conn, schema); err != nil {
t.Error("Migrate:", err)
}
}(i)
}

// Migrate and issue writes on one connection.
conn, err := sqlite.OpenConn(dbPath, sqlite.OpenReadWrite, sqlite.OpenCreate)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := conn.Close(); err != nil {
t.Error(err)
}
}()
if err := Migrate(ctx, conn, schema); err != nil {
t.Fatal("Migrate:", err)
}
for i := 0; i < 150; i++ {
if err := sqlitex.Execute(conn, "insert into foo values (?)", &sqlitex.ExecOptions{
Args: []any{i},
}); err != nil {
t.Error("insert query:", err)
}
}
})
}

// withTestConn makes an independent connection to the given database.
Expand Down