| // Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. |
| // |
| // Use of this source code is governed by an MIT-style |
| // license that can be found in the LICENSE file. |
| |
| //go:build go1.8 && cgo |
| // +build go1.8,cgo |
| |
| package sqlite3 |
| |
| import ( |
| "context" |
| "database/sql" |
| "fmt" |
| "io/ioutil" |
| "math/rand" |
| "os" |
| "sync" |
| "testing" |
| "time" |
| ) |
| |
| func TestNamedParams(t *testing.T) { |
| tempFilename := TempFilename(t) |
| defer os.Remove(tempFilename) |
| db, err := sql.Open("sqlite3", tempFilename) |
| if err != nil { |
| t.Fatal("Failed to open database:", err) |
| } |
| defer db.Close() |
| |
| _, err = db.Exec(` |
| create table foo (id integer, name text, extra text); |
| `) |
| if err != nil { |
| t.Error("Failed to call db.Query:", err) |
| } |
| |
| _, err = db.Exec(`insert into foo(id, name, extra) values(:id, :name, :name)`, sql.Named("name", "foo"), sql.Named("id", 1)) |
| if err != nil { |
| t.Error("Failed to call db.Exec:", err) |
| } |
| |
| row := db.QueryRow(`select id, extra from foo where id = :id and extra = :extra`, sql.Named("id", 1), sql.Named("extra", "foo")) |
| if row == nil { |
| t.Error("Failed to call db.QueryRow") |
| } |
| var id int |
| var extra string |
| err = row.Scan(&id, &extra) |
| if err != nil { |
| t.Error("Failed to db.Scan:", err) |
| } |
| if id != 1 || extra != "foo" { |
| t.Error("Failed to db.QueryRow: not matched results") |
| } |
| } |
| |
| var ( |
| testTableStatements = []string{ |
| `DROP TABLE IF EXISTS test_table`, |
| ` |
| CREATE TABLE IF NOT EXISTS test_table ( |
| key1 VARCHAR(64) PRIMARY KEY, |
| key_id VARCHAR(64) NOT NULL, |
| key2 VARCHAR(64) NOT NULL, |
| key3 VARCHAR(64) NOT NULL, |
| key4 VARCHAR(64) NOT NULL, |
| key5 VARCHAR(64) NOT NULL, |
| key6 VARCHAR(64) NOT NULL, |
| data BLOB NOT NULL |
| );`, |
| } |
| letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" |
| ) |
| |
| func randStringBytes(n int) string { |
| b := make([]byte, n) |
| for i := range b { |
| b[i] = letterBytes[rand.Intn(len(letterBytes))] |
| } |
| return string(b) |
| } |
| |
| func initDatabase(t *testing.T, db *sql.DB, rowCount int64) { |
| for _, query := range testTableStatements { |
| _, err := db.Exec(query) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| for i := int64(0); i < rowCount; i++ { |
| query := `INSERT INTO test_table |
| (key1, key_id, key2, key3, key4, key5, key6, data) |
| VALUES |
| (?, ?, ?, ?, ?, ?, ?, ?);` |
| args := []interface{}{ |
| randStringBytes(50), |
| fmt.Sprint(i), |
| randStringBytes(50), |
| randStringBytes(50), |
| randStringBytes(50), |
| randStringBytes(50), |
| randStringBytes(50), |
| randStringBytes(50), |
| randStringBytes(2048), |
| } |
| _, err := db.Exec(query, args...) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| } |
| |
| func TestShortTimeout(t *testing.T) { |
| srcTempFilename := TempFilename(t) |
| defer os.Remove(srcTempFilename) |
| |
| db, err := sql.Open("sqlite3", srcTempFilename) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer db.Close() |
| initDatabase(t, db, 100) |
| |
| ctx, cancel := context.WithTimeout(context.Background(), 1*time.Microsecond) |
| defer cancel() |
| query := `SELECT key1, key_id, key2, key3, key4, key5, key6, data |
| FROM test_table |
| ORDER BY key2 ASC` |
| _, err = db.QueryContext(ctx, query) |
| if err != nil && err != context.DeadlineExceeded { |
| t.Fatal(err) |
| } |
| if ctx.Err() != nil && ctx.Err() != context.DeadlineExceeded { |
| t.Fatal(ctx.Err()) |
| } |
| } |
| |
| func TestExecContextCancel(t *testing.T) { |
| srcTempFilename := TempFilename(t) |
| defer os.Remove(srcTempFilename) |
| |
| db, err := sql.Open("sqlite3", srcTempFilename) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| defer db.Close() |
| |
| ts := time.Now() |
| initDatabase(t, db, 1000) |
| spent := time.Since(ts) |
| const minTestTime = 100 * time.Millisecond |
| if spent < minTestTime { |
| t.Skipf("test will be too racy (spent=%s < min=%s) as ExecContext below will be too fast.", |
| spent.String(), minTestTime.String(), |
| ) |
| } |
| |
| // expected to be extremely slow query |
| q := ` |
| INSERT INTO test_table (key1, key_id, key2, key3, key4, key5, key6, data) |
| SELECT t1.key1 || t2.key1, t1.key_id || t2.key_id, t1.key2 || t2.key2, t1.key3 || t2.key3, t1.key4 || t2.key4, t1.key5 || t2.key5, t1.key6 || t2.key6, t1.data || t2.data |
| FROM test_table t1 LEFT OUTER JOIN test_table t2` |
| // expect query above take ~ same time as setup above |
| // This is racy: the context must be valid so sql/db.ExecContext calls the sqlite3 driver. |
| // It starts the query, the context expires, then calls sqlite3_interrupt |
| ctx, cancel := context.WithTimeout(context.Background(), minTestTime/2) |
| defer cancel() |
| ts = time.Now() |
| r, err := db.ExecContext(ctx, q) |
| // racy check |
| if r != nil { |
| n, err := r.RowsAffected() |
| t.Logf("query should not have succeeded: rows=%d; err=%v; duration=%s", |
| n, err, time.Since(ts).String()) |
| } |
| if err != context.DeadlineExceeded { |
| t.Fatal(err, ctx.Err()) |
| } |
| } |
| |
| func TestQueryRowContextCancel(t *testing.T) { |
| srcTempFilename := TempFilename(t) |
| defer os.Remove(srcTempFilename) |
| |
| db, err := sql.Open("sqlite3", srcTempFilename) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer db.Close() |
| initDatabase(t, db, 100) |
| |
| const query = `SELECT key_id FROM test_table ORDER BY key2 ASC` |
| var keyID string |
| unexpectedErrors := make(map[string]int) |
| for i := 0; i < 10000; i++ { |
| ctx, cancel := context.WithCancel(context.Background()) |
| row := db.QueryRowContext(ctx, query) |
| |
| cancel() |
| // it is fine to get "nil" as context cancellation can be handled with delay |
| if err := row.Scan(&keyID); err != nil && err != context.Canceled { |
| if err.Error() == "sql: Rows are closed" { |
| // see https://github.com/golang/go/issues/24431 |
| // fixed in 1.11.1 to properly return context error |
| continue |
| } |
| unexpectedErrors[err.Error()]++ |
| } |
| } |
| for errText, count := range unexpectedErrors { |
| t.Error(errText, count) |
| } |
| } |
| |
| func TestQueryRowContextCancelParallel(t *testing.T) { |
| srcTempFilename := TempFilename(t) |
| defer os.Remove(srcTempFilename) |
| |
| db, err := sql.Open("sqlite3", srcTempFilename) |
| if err != nil { |
| t.Fatal(err) |
| } |
| db.SetMaxOpenConns(10) |
| db.SetMaxIdleConns(5) |
| |
| defer db.Close() |
| initDatabase(t, db, 100) |
| |
| const query = `SELECT key_id FROM test_table ORDER BY key2 ASC` |
| wg := sync.WaitGroup{} |
| defer wg.Wait() |
| |
| testCtx, cancel := context.WithCancel(context.Background()) |
| defer cancel() |
| |
| for i := 0; i < 10; i++ { |
| wg.Add(1) |
| go func() { |
| defer wg.Done() |
| |
| var keyID string |
| for { |
| select { |
| case <-testCtx.Done(): |
| return |
| default: |
| } |
| ctx, cancel := context.WithCancel(context.Background()) |
| row := db.QueryRowContext(ctx, query) |
| |
| cancel() |
| _ = row.Scan(&keyID) // see TestQueryRowContextCancel |
| } |
| }() |
| } |
| |
| var keyID string |
| for i := 0; i < 10000; i++ { |
| // note that testCtx is not cancelled during query execution |
| row := db.QueryRowContext(testCtx, query) |
| |
| if err := row.Scan(&keyID); err != nil { |
| t.Fatal(i, err) |
| } |
| } |
| } |
| |
| func TestExecCancel(t *testing.T) { |
| db, err := sql.Open("sqlite3", ":memory:") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer db.Close() |
| |
| if _, err = db.Exec("create table foo (id integer primary key)"); err != nil { |
| t.Fatal(err) |
| } |
| |
| for n := 0; n < 100; n++ { |
| ctx, cancel := context.WithCancel(context.Background()) |
| _, err = db.ExecContext(ctx, "insert into foo (id) values (?)", n) |
| cancel() |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| } |
| |
| func doTestOpenContext(t *testing.T, option string) (string, error) { |
| tempFilename := TempFilename(t) |
| url := tempFilename + option |
| |
| defer func() { |
| err := os.Remove(tempFilename) |
| if err != nil { |
| t.Error("temp file remove error:", err) |
| } |
| }() |
| |
| db, err := sql.Open("sqlite3", url) |
| if err != nil { |
| return "Failed to open database:", err |
| } |
| |
| defer func() { |
| err = db.Close() |
| if err != nil { |
| t.Error("db close error:", err) |
| } |
| }() |
| |
| ctx, cancel := context.WithTimeout(context.Background(), 55*time.Second) |
| err = db.PingContext(ctx) |
| cancel() |
| if err != nil { |
| return "ping error:", err |
| } |
| |
| ctx, cancel = context.WithTimeout(context.Background(), 55*time.Second) |
| _, err = db.ExecContext(ctx, "drop table foo") |
| cancel() |
| ctx, cancel = context.WithTimeout(context.Background(), 55*time.Second) |
| _, err = db.ExecContext(ctx, "create table foo (id integer)") |
| cancel() |
| if err != nil { |
| return "Failed to create table:", err |
| } |
| |
| if stat, err := os.Stat(tempFilename); err != nil || stat.IsDir() { |
| return "Failed to create ./foo.db", nil |
| } |
| |
| return "", nil |
| } |
| |
| func TestOpenContext(t *testing.T) { |
| cases := map[string]bool{ |
| "": true, |
| "?_txlock=immediate": true, |
| "?_txlock=deferred": true, |
| "?_txlock=exclusive": true, |
| "?_txlock=bogus": false, |
| } |
| for option, expectedPass := range cases { |
| result, err := doTestOpenContext(t, option) |
| if result == "" { |
| if !expectedPass { |
| errmsg := fmt.Sprintf("_txlock error not caught at dbOpen with option: %s", option) |
| t.Fatal(errmsg) |
| } |
| } else if expectedPass { |
| if err == nil { |
| t.Fatal(result) |
| } else { |
| t.Fatal(result, err) |
| } |
| } |
| } |
| } |
| |
| func TestFileCopyTruncate(t *testing.T) { |
| var err error |
| tempFilename := TempFilename(t) |
| |
| defer func() { |
| err = os.Remove(tempFilename) |
| if err != nil { |
| t.Error("temp file remove error:", err) |
| } |
| }() |
| |
| db, err := sql.Open("sqlite3", tempFilename) |
| if err != nil { |
| t.Fatal("open error:", err) |
| } |
| |
| defer func() { |
| err = db.Close() |
| if err != nil { |
| t.Error("db close error:", err) |
| } |
| }() |
| |
| ctx, cancel := context.WithTimeout(context.Background(), 55*time.Second) |
| err = db.PingContext(ctx) |
| cancel() |
| if err != nil { |
| t.Fatal("ping error:", err) |
| } |
| |
| ctx, cancel = context.WithTimeout(context.Background(), 55*time.Second) |
| _, err = db.ExecContext(ctx, "drop table foo") |
| cancel() |
| ctx, cancel = context.WithTimeout(context.Background(), 55*time.Second) |
| _, err = db.ExecContext(ctx, "create table foo (id integer)") |
| cancel() |
| if err != nil { |
| t.Fatal("create table error:", err) |
| } |
| |
| // copy db to new file |
| var data []byte |
| data, err = ioutil.ReadFile(tempFilename) |
| if err != nil { |
| t.Fatal("read file error:", err) |
| } |
| |
| var f *os.File |
| f, err = os.Create(tempFilename + "-db-copy") |
| if err != nil { |
| t.Fatal("create file error:", err) |
| } |
| |
| defer func() { |
| err = os.Remove(tempFilename + "-db-copy") |
| if err != nil { |
| t.Error("temp file moved remove error:", err) |
| } |
| }() |
| |
| _, err = f.Write(data) |
| if err != nil { |
| f.Close() |
| t.Fatal("write file error:", err) |
| } |
| err = f.Close() |
| if err != nil { |
| t.Fatal("close file error:", err) |
| } |
| |
| // truncate current db file |
| f, err = os.OpenFile(tempFilename, os.O_WRONLY|os.O_TRUNC, 0666) |
| if err != nil { |
| t.Fatal("open file error:", err) |
| } |
| err = f.Close() |
| if err != nil { |
| t.Fatal("close file error:", err) |
| } |
| |
| // test db after file truncate |
| ctx, cancel = context.WithTimeout(context.Background(), 55*time.Second) |
| err = db.PingContext(ctx) |
| cancel() |
| if err != nil { |
| t.Fatal("ping error:", err) |
| } |
| |
| ctx, cancel = context.WithTimeout(context.Background(), 55*time.Second) |
| _, err = db.ExecContext(ctx, "drop table foo") |
| cancel() |
| if err == nil { |
| t.Fatal("drop table no error") |
| } |
| |
| ctx, cancel = context.WithTimeout(context.Background(), 55*time.Second) |
| _, err = db.ExecContext(ctx, "create table foo (id integer)") |
| cancel() |
| if err != nil { |
| t.Fatal("create table error:", err) |
| } |
| |
| err = db.Close() |
| if err != nil { |
| t.Error("db close error:", err) |
| } |
| |
| // test copied file |
| db, err = sql.Open("sqlite3", tempFilename+"-db-copy") |
| if err != nil { |
| t.Fatal("open error:", err) |
| } |
| |
| defer func() { |
| err = db.Close() |
| if err != nil { |
| t.Error("db close error:", err) |
| } |
| }() |
| |
| ctx, cancel = context.WithTimeout(context.Background(), 55*time.Second) |
| err = db.PingContext(ctx) |
| cancel() |
| if err != nil { |
| t.Fatal("ping error:", err) |
| } |
| |
| ctx, cancel = context.WithTimeout(context.Background(), 55*time.Second) |
| _, err = db.ExecContext(ctx, "drop table foo") |
| cancel() |
| if err != nil { |
| t.Fatal("drop table error:", err) |
| } |
| |
| ctx, cancel = context.WithTimeout(context.Background(), 55*time.Second) |
| _, err = db.ExecContext(ctx, "create table foo (id integer)") |
| cancel() |
| if err != nil { |
| t.Fatal("create table error:", err) |
| } |
| } |