| //go:build cgo |
| // +build cgo |
| |
| package sqlite3 |
| |
| import ( |
| "context" |
| "database/sql" |
| "database/sql/driver" |
| "errors" |
| "fmt" |
| "io" |
| "sync" |
| "sync/atomic" |
| "testing" |
| "time" |
| ) |
| |
| const contextTestTimeout = 5 * time.Second |
| const contextTestMaxRows = int64(1 << 60) |
| |
| const contextTestControlledQuery = ` |
| WITH RECURSIVE numbers(value) AS ( |
| VALUES(0) |
| UNION ALL |
| SELECT value + 1 FROM numbers |
| WHERE value < ? AND context_cancel_continue() |
| ) |
| SELECT sum(value) FROM numbers` |
| |
| func TestRowsContextCancelDuringStep(t *testing.T) { |
| for _, laterRow := range []bool{false, true} { |
| t.Run(fmt.Sprintf("laterRow=%t", laterRow), func(t *testing.T) { |
| query := contextTestControlledQuery |
| if laterRow { |
| query = "SELECT 1 UNION ALL SELECT (" + query + ")" |
| } |
| conn := openContextTestConn(t) |
| started, stopQuery := registerContextTestQuery(t, conn) |
| |
| ctx, cancel := context.WithCancel(context.Background()) |
| // Run both the eager first step and a subsequent Next under cancellation. |
| nextDone := make(chan error, 1) |
| go func() { |
| rows, err := conn.QueryContext(ctx, query, contextTestQueryArgs(contextTestMaxRows)) |
| if err != nil { |
| nextDone <- err |
| return |
| } |
| defer rows.Close() |
| if laterRow { |
| if err := rows.Next(make([]driver.Value, 1)); err != nil { |
| nextDone <- err |
| return |
| } |
| } |
| nextDone <- rows.Next(make([]driver.Value, 1)) |
| }() |
| |
| select { |
| case <-started: |
| case <-time.After(contextTestTimeout): |
| cancel() |
| _ = stopContextTestQuery(t, stopQuery, nextDone) |
| t.Fatal("query did not start") |
| } |
| cancel() |
| |
| select { |
| case err := <-nextDone: |
| if !errors.Is(err, context.Canceled) { |
| t.Fatalf("Next error = %v, want context.Canceled", err) |
| } |
| case <-time.After(contextTestTimeout): |
| _ = stopContextTestQuery(t, stopQuery, nextDone) |
| t.Fatal("Next did not return after cancellation") |
| } |
| }) |
| } |
| } |
| |
| func TestRowsContextCancelBetweenRows(t *testing.T) { |
| conn := openContextTestConn(t) |
| ctx, cancel := context.WithCancel(context.Background()) |
| rows, err := conn.QueryContext(ctx, "SELECT 1 UNION ALL SELECT 2", nil) |
| if err != nil { |
| t.Fatalf("query: %v", err) |
| } |
| defer rows.Close() |
| |
| values := make([]driver.Value, 1) |
| if err := rows.Next(values); err != nil { |
| t.Fatalf("first Next: %v", err) |
| } |
| cancel() |
| if err := rows.Next(values); !errors.Is(err, context.Canceled) { |
| t.Fatalf("second Next error = %v, want context.Canceled", err) |
| } |
| } |
| |
| func TestRowsInterruptIgnoresInactiveRows(t *testing.T) { |
| conn := openContextTestConn(t) |
| |
| idleCtx, cancelIdle := context.WithCancel(context.Background()) |
| defer cancelIdle() |
| idleRows, err := conn.QueryContext(idleCtx, "SELECT 1 UNION ALL SELECT 2", nil) |
| if err != nil { |
| t.Fatalf("query idle rows: %v", err) |
| } |
| defer idleRows.Close() |
| if err := idleRows.Next(make([]driver.Value, 1)); err != nil { |
| t.Fatalf("read idle rows: %v", err) |
| } |
| |
| started, stopQuery := registerContextTestQuery(t, conn) |
| activeCtx, cancelActive := context.WithCancel(context.Background()) |
| defer cancelActive() |
| nextDone := make(chan error, 1) |
| activeRowsCh := make(chan *SQLiteRows, 1) |
| go func() { |
| activeRows, err := conn.QueryContext(activeCtx, contextTestControlledQuery, contextTestQueryArgs(contextTestMaxRows)) |
| if err != nil { |
| nextDone <- err |
| return |
| } |
| defer activeRows.Close() |
| activeRowsCh <- activeRows.(*SQLiteRows) |
| nextDone <- activeRows.Next(make([]driver.Value, 1)) |
| }() |
| select { |
| case <-started: |
| case <-time.After(contextTestTimeout): |
| _ = stopContextTestQuery(t, stopQuery, nextDone) |
| t.Fatal("active query did not start") |
| } |
| |
| // Invoke the callback guard directly so the test does not depend on scheduling. |
| conn.interruptActiveRows(idleRows.(*SQLiteRows)) |
| if err := stopContextTestQuery(t, stopQuery, nextDone); err != nil { |
| t.Fatalf("active query was interrupted: %v", err) |
| } |
| select { |
| case rows := <-activeRowsCh: |
| _ = rows |
| default: |
| } |
| } |
| |
| func TestRowsLateInterruptDoesNotAffectReusedStatement(t *testing.T) { |
| conn := openContextTestConn(t) |
| started, stopQuery := registerContextTestQuery(t, conn) |
| stmtDriver, err := conn.Prepare(contextTestControlledQuery) |
| if err != nil { |
| t.Fatalf("prepare: %v", err) |
| } |
| defer stmtDriver.Close() |
| stmt := stmtDriver.(*SQLiteStmt) |
| |
| oldCtx, cancelOld := context.WithCancel(context.Background()) |
| defer cancelOld() |
| oldRows, err := stmt.QueryContext(oldCtx, contextTestQueryArgs(0)) |
| if err != nil { |
| t.Fatalf("query old rows: %v", err) |
| } |
| if err := oldRows.Next(make([]driver.Value, 1)); err != nil { |
| t.Fatalf("read old rows: %v", err) |
| } |
| if err := oldRows.Close(); err != nil { |
| t.Fatalf("close old rows: %v", err) |
| } |
| |
| newCtx, cancelNew := context.WithCancel(context.Background()) |
| defer cancelNew() |
| nextDone := make(chan error, 1) |
| go func() { |
| newRows, err := stmt.QueryContext(newCtx, contextTestQueryArgs(contextTestMaxRows)) |
| if err != nil { |
| nextDone <- err |
| return |
| } |
| defer newRows.Close() |
| nextDone <- newRows.Next(make([]driver.Value, 1)) |
| }() |
| select { |
| case <-started: |
| case <-time.After(contextTestTimeout): |
| _ = stopContextTestQuery(t, stopQuery, nextDone) |
| t.Fatal("replacement query did not start") |
| } |
| |
| // Simulate a callback that started before oldRows was closed. |
| conn.interruptActiveRows(oldRows.(*SQLiteRows)) |
| if err := stopContextTestQuery(t, stopQuery, nextDone); err != nil { |
| t.Fatalf("replacement query was interrupted: %v", err) |
| } |
| } |
| |
| func TestRowsContextCancelAndClose(t *testing.T) { |
| conn := openContextTestConn(t) |
| for i := 0; i < 100; i++ { |
| ctx, cancel := context.WithCancel(context.Background()) |
| rows, err := conn.QueryContext(ctx, "SELECT 1 UNION ALL SELECT 2", nil) |
| if err != nil { |
| t.Fatalf("query cancellable rows: %v", err) |
| } |
| if err := rows.Next(make([]driver.Value, 1)); err != nil { |
| t.Fatalf("read cancellable rows: %v", err) |
| } |
| |
| start := make(chan struct{}) |
| cancelDone := make(chan struct{}) |
| closeDone := make(chan error, 1) |
| go func() { |
| <-start |
| closeDone <- rows.Close() |
| }() |
| go func() { |
| <-start |
| cancel() |
| close(cancelDone) |
| }() |
| close(start) |
| select { |
| case err := <-closeDone: |
| if err != nil { |
| t.Fatalf("close cancellable rows: %v", err) |
| } |
| case <-time.After(contextTestTimeout): |
| t.Fatal("close cancellable rows timed out") |
| } |
| select { |
| case <-cancelDone: |
| case <-time.After(contextTestTimeout): |
| t.Fatal("cancel cancellable rows timed out") |
| } |
| |
| reusedRows, err := conn.Query("SELECT 1", nil) |
| if err != nil { |
| t.Fatalf("query reused connection: %v", err) |
| } |
| if err := reusedRows.Next(make([]driver.Value, 1)); err != nil { |
| t.Fatalf("read reused connection: %v", err) |
| } |
| if err := reusedRows.Close(); err != nil { |
| t.Fatalf("close reused rows: %v", err) |
| } |
| } |
| } |
| |
| func TestRowsContextPanicClearsActiveRows(t *testing.T) { |
| conn := openContextTestConn(t) |
| const panicValue = "context test panic" |
| if err := conn.RegisterFunc("context_cancel_panic", func() int64 { |
| panic(panicValue) |
| }, false); err != nil { |
| t.Fatalf("register function: %v", err) |
| } |
| |
| ctx, cancel := context.WithCancel(context.Background()) |
| defer cancel() |
| |
| // The first step runs inside QueryContext, so the panic surfaces |
| // there rather than at Next. |
| var gotPanic any |
| func() { |
| defer func() { |
| gotPanic = recover() |
| }() |
| rows, err := conn.QueryContext(ctx, "SELECT context_cancel_panic()", nil) |
| if err != nil { |
| t.Fatalf("query: %v", err) |
| } |
| defer rows.Close() |
| _ = rows.Next(make([]driver.Value, 1)) |
| }() |
| if gotPanic != panicValue { |
| t.Fatalf("Next panic = %v, want %q", gotPanic, panicValue) |
| } |
| |
| conn.mu.Lock() |
| activeRows := conn.activeRows |
| conn.mu.Unlock() |
| if activeRows != nil { |
| t.Fatalf("active rows = %p, want nil", activeRows) |
| } |
| } |
| |
| func TestDatabaseSQLRowsContextCancelAndReuse(t *testing.T) { |
| db, err := sql.Open("sqlite3", ":memory:") |
| if err != nil { |
| t.Fatalf("open database: %v", err) |
| } |
| defer db.Close() |
| |
| sqlConn, err := db.Conn(context.Background()) |
| if err != nil { |
| t.Fatalf("get database connection: %v", err) |
| } |
| defer sqlConn.Close() |
| |
| var started <-chan struct{} |
| var stopQuery func() |
| if err := sqlConn.Raw(func(driverConn any) error { |
| sqliteConn, ok := driverConn.(*SQLiteConn) |
| if !ok { |
| return fmt.Errorf("driver connection type = %T, want *SQLiteConn", driverConn) |
| } |
| started, stopQuery = registerContextTestQuery(t, sqliteConn) |
| return nil |
| }); err != nil { |
| t.Fatalf("access driver connection: %v", err) |
| } |
| |
| ctx, cancel := context.WithCancel(context.Background()) |
| defer cancel() |
| nextDone := make(chan error, 1) |
| go func() { |
| rows, err := sqlConn.QueryContext(ctx, contextTestControlledQuery, contextTestMaxRows) |
| if err != nil { |
| nextDone <- err |
| return |
| } |
| if rows.Next() { |
| rows.Close() |
| nextDone <- errors.New("Next returned a row after cancellation") |
| return |
| } |
| err = rows.Err() |
| if closeErr := rows.Close(); err == nil { |
| err = closeErr |
| } |
| nextDone <- err |
| }() |
| select { |
| case <-started: |
| case <-time.After(contextTestTimeout): |
| cancel() |
| _ = stopContextTestQuery(t, stopQuery, nextDone) |
| t.Fatal("query did not start") |
| } |
| cancel() |
| |
| select { |
| case err := <-nextDone: |
| if !errors.Is(err, context.Canceled) { |
| t.Fatalf("Next error = %v, want context.Canceled", err) |
| } |
| case <-time.After(contextTestTimeout): |
| _ = stopContextTestQuery(t, stopQuery, nextDone) |
| t.Fatal("Next did not return after cancellation") |
| } |
| |
| var got int |
| if err := sqlConn.QueryRowContext(context.Background(), "SELECT 1").Scan(&got); err != nil { |
| t.Fatalf("query reused connection: %v", err) |
| } |
| if got != 1 { |
| t.Fatalf("reused query = %d, want 1", got) |
| } |
| } |
| |
| func registerContextTestQuery(tb testing.TB, conn *SQLiteConn) (<-chan struct{}, func()) { |
| tb.Helper() |
| started := make(chan struct{}) |
| var startedOnce sync.Once |
| var keepRunning atomic.Bool |
| keepRunning.Store(true) |
| if err := conn.RegisterFunc("context_cancel_continue", func() int64 { |
| startedOnce.Do(func() { close(started) }) |
| if keepRunning.Load() { |
| return 1 |
| } |
| return 0 |
| }, false); err != nil { |
| tb.Fatalf("register function: %v", err) |
| } |
| return started, func() { |
| keepRunning.Store(false) |
| } |
| } |
| |
| func stopContextTestQuery(tb testing.TB, stop func(), nextDone <-chan error) error { |
| tb.Helper() |
| stop() |
| select { |
| case err := <-nextDone: |
| return err |
| case <-time.After(contextTestTimeout): |
| tb.Fatal("controlled query did not stop") |
| return nil |
| } |
| } |
| |
| func contextTestQueryArgs(maxRows int64) []driver.NamedValue { |
| return []driver.NamedValue{{Ordinal: 1, Value: maxRows}} |
| } |
| |
| func BenchmarkRowsContext(b *testing.B) { |
| conn := openContextTestConn(b) |
| if _, err := conn.Exec(` |
| CREATE TABLE benchmark_rows(value INTEGER PRIMARY KEY); |
| WITH RECURSIVE numbers(value) AS ( |
| VALUES(1) |
| UNION ALL |
| SELECT value + 1 FROM numbers WHERE value < 1000 |
| ) |
| INSERT INTO benchmark_rows SELECT value FROM numbers`, nil); err != nil { |
| b.Fatalf("populate benchmark rows: %v", err) |
| } |
| stmtDriver, err := conn.Prepare("SELECT value FROM benchmark_rows LIMIT ?") |
| if err != nil { |
| b.Fatalf("prepare: %v", err) |
| } |
| defer stmtDriver.Close() |
| stmt := stmtDriver.(*SQLiteStmt) |
| |
| contexts := []struct { |
| name string |
| new func() (context.Context, context.CancelFunc) |
| }{ |
| { |
| name: "non_cancelable", |
| new: func() (context.Context, context.CancelFunc) { |
| return context.Background(), func() {} |
| }, |
| }, |
| { |
| name: "cancelable", |
| new: func() (context.Context, context.CancelFunc) { |
| return context.WithCancel(context.Background()) |
| }, |
| }, |
| } |
| |
| for _, rowCount := range []int64{0, 1, 1000} { |
| for _, benchmarkContext := range contexts { |
| b.Run(fmt.Sprintf("rows=%d/context=%s", rowCount, benchmarkContext.name), func(b *testing.B) { |
| ctx, cancel := benchmarkContext.new() |
| defer cancel() |
| args := []driver.NamedValue{{Ordinal: 1, Value: rowCount}} |
| values := make([]driver.Value, 1) |
| b.ReportAllocs() |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| rows, err := stmt.QueryContext(ctx, args) |
| if err != nil { |
| b.Fatalf("query: %v", err) |
| } |
| gotRows := int64(0) |
| for { |
| err := rows.Next(values) |
| if errors.Is(err, io.EOF) { |
| break |
| } |
| if err != nil { |
| b.Fatalf("Next: %v", err) |
| } |
| gotRows++ |
| } |
| if err := rows.Close(); err != nil { |
| b.Fatalf("close rows: %v", err) |
| } |
| if gotRows != rowCount { |
| b.Fatalf("row count = %d, want %d", gotRows, rowCount) |
| } |
| } |
| b.ReportMetric(float64(rowCount), "rows/op") |
| }) |
| } |
| } |
| |
| for _, benchmarkContext := range contexts { |
| b.Run(fmt.Sprintf("close_without_next/context=%s", benchmarkContext.name), func(b *testing.B) { |
| ctx, cancel := benchmarkContext.new() |
| defer cancel() |
| args := []driver.NamedValue{{Ordinal: 1, Value: int64(1)}} |
| b.ReportAllocs() |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| rows, err := stmt.QueryContext(ctx, args) |
| if err != nil { |
| b.Fatalf("query: %v", err) |
| } |
| if err := rows.Close(); err != nil { |
| b.Fatalf("close rows: %v", err) |
| } |
| } |
| }) |
| } |
| } |
| |
| func openContextTestConn(tb testing.TB) *SQLiteConn { |
| tb.Helper() |
| rawConn, err := (&SQLiteDriver{}).Open(":memory:") |
| if err != nil { |
| tb.Fatalf("open SQLite connection: %v", err) |
| } |
| conn := rawConn.(*SQLiteConn) |
| tb.Cleanup(func() { |
| if err := conn.Close(); err != nil { |
| tb.Errorf("close SQLite connection: %v", err) |
| } |
| }) |
| return conn |
| } |