diff --git a/sqlite3.go b/sqlite3.go index a967cab0..dd016a5c 100644 --- a/sqlite3.go +++ b/sqlite3.go @@ -962,11 +962,16 @@ func (c *SQLiteConn) query(ctx context.Context, query string, args []driver.Name // Begin transaction. func (c *SQLiteConn) Begin() (driver.Tx, error) { - return c.begin(context.Background()) + return c.begin(context.Background(), false) } -func (c *SQLiteConn) begin(ctx context.Context) (driver.Tx, error) { - if _, err := c.exec(ctx, c.txlock, nil); err != nil { +func (c *SQLiteConn) begin(ctx context.Context, ro bool) (driver.Tx, error) { + txlock := c.txlock + if ro { + txlock = "BEGIN" + } + + if _, err := c.exec(ctx, txlock, nil); err != nil { return nil, err } return &SQLiteTx{c}, nil diff --git a/sqlite3_go18.go b/sqlite3_go18.go index 34cad08e..32dbe081 100644 --- a/sqlite3_go18.go +++ b/sqlite3_go18.go @@ -40,7 +40,7 @@ func (c *SQLiteConn) PrepareContext(ctx context.Context, query string) (driver.S // BeginTx implement ConnBeginTx. func (c *SQLiteConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { - return c.begin(ctx) + return c.begin(ctx, opts.ReadOnly) } // QueryContext implement QueryerContext. diff --git a/sqlite3_go18_test.go b/sqlite3_go18_test.go index eec7479d..ff4b0dda 100644 --- a/sqlite3_go18_test.go +++ b/sqlite3_go18_test.go @@ -11,6 +11,7 @@ package sqlite3 import ( "context" "database/sql" + "errors" "fmt" "io/ioutil" "math/rand" @@ -502,3 +503,99 @@ func TestFileCopyTruncate(t *testing.T) { t.Fatal("create table error:", err) } } + +func TestBeginReadOnly(t *testing.T) { + srcTempFilename := TempFilename(t) + defer os.Remove(srcTempFilename) + + // Setup a database with WAL journaling and immediate locking to support concurrent read transactions with an active read-write transaction + db, err := sql.Open("sqlite3", srcTempFilename+"?_journal_mode=WAL&_busy_timeout=-1&_txlock=immediate") + if err != nil { + t.Fatal(err) + } + + db.SetMaxOpenConns(10) + db.SetMaxIdleConns(5) + + defer db.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Begin an IMMEDIATE transaction that holds the write lock + _, err = db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + t.Fatal("Failed to create IMMEDIATE transaction (1):", err) + } + + // Attempting to create a concurrent IMMEDIATE transaction should fail immediately with SQLITE_BUSY + _, err = db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + var sqlerr Error + if !errors.As(err, &sqlerr) { + t.Fatalf("IMMEDIATE transaction (2) should have returned an sqlite3.Error. Got %T", err) + } + + if sqlerr.Code&ErrBusy != ErrBusy { + t.Fatalf("IMMEDIATE transaction (2) should have returned an ErrBusy code. Got %q", sqlerr.Error()) + } + + } else { + t.Fatal("IMMEDIATE transaction (2) should have failed") + } + + // Begin multiple DEFERRED transactions with an active read-write transaction + _, err = db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}) + if err != nil { + t.Fatal("Failed to create DEFERRED transaction (1):", err) + } + + _, err = db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}) + if err != nil { + t.Fatal("Failed to create DEFERRED transaction (2):", err) + } + + _, err = db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}) + if err != nil { + t.Fatal("Failed to create DEFERRED transaction (3):", err) + } +} + +func TestBeginTimeout(t *testing.T) { + srcTempFilename := TempFilename(t) + defer os.Remove(srcTempFilename) + + // Setup a database with WAL journaling, immediate locking, and a busy_timeout to support blocking concurrent calls to BeginTx + db, err := sql.Open("sqlite3", srcTempFilename+"?_journal_mode=WAL&_busy_timeout=1000&_txlock=immediate") + if err != nil { + t.Fatal(err) + } + + db.SetMaxOpenConns(10) + db.SetMaxIdleConns(5) + + defer db.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + start := time.Now() + + // Begin an IMMEDIATE transaction that holds the write lock for 100ms + timeout, _ := context.WithTimeout(ctx, 100*time.Millisecond) + _, err = db.BeginTx(timeout, &sql.TxOptions{}) + if err != nil { + t.Fatal("Failed to create IMMEDIATE transaction (1):", err) + } + + // Begin a second IMMEDIATE transaction that blocks until the first is closed + _, err = db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + t.Fatal("Failed to create IMMEDIATE transaction (2):", err) + } + + elapsed := time.Since(start) + if elapsed < 100*time.Millisecond { + t.Fatalf("Beginning IMMEDIATE transaction (2) should have blocked for at least 100 milliseconds. Got %s", elapsed) + } +}