diff --git a/src/database/sql/example_cli_test.go b/src/database/sql/example_cli_test.go new file mode 100644 index 00000000000..8c61d755bb8 --- /dev/null +++ b/src/database/sql/example_cli_test.go @@ -0,0 +1,86 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sql_test + +import ( + "context" + "database/sql" + "flag" + "log" + "os" + "os/signal" + "time" +) + +var pool *sql.DB // Database connection pool. + +func Example_openDBCLI() { + id := flag.Int64("id", 0, "person ID to find") + dsn := flag.String("dsn", os.Getenv("DSN"), "connection data source name") + flag.Parse() + + if len(*dsn) == 0 { + log.Fatal("missing dsn flag") + } + if *id == 0 { + log.Fatal("missing person ID") + } + var err error + + // Opening a driver typically will not attempt to connect to the database. + pool, err = sql.Open("driver-name", *dsn) + if err != nil { + // This will not be a connection error, but a DSN parse error or + // another initialization error. + log.Fatal("unable to use data source name", err) + } + defer pool.Close() + + pool.SetConnMaxLifetime(0) + pool.SetMaxIdleConns(3) + pool.SetMaxOpenConns(3) + + ctx, stop := context.WithCancel(context.Background()) + defer stop() + + appSignal := make(chan os.Signal, 3) + signal.Notify(appSignal, os.Interrupt) + + go func() { + select { + case <-appSignal: + stop() + } + }() + + Ping(ctx) + + Query(ctx, *id) +} + +// Ping the database to verify DSN provided by the user is valid and the +// server accessible. If the ping fails exit the program with an error. +func Ping(ctx context.Context) { + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + + if err := pool.PingContext(ctx); err != nil { + log.Fatalf("unable to connect to database: %v", err) + } +} + +// Query the database for the information requested and prints the results. +// If the query fails exit the program with an error. +func Query(ctx context.Context, id int64) { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + var name string + err := pool.QueryRowContext(ctx, "select p.name from people as p where p.id = :id;", sql.Named("id", id)).Scan(&name) + if err != nil { + log.Fatal("unable to execute search query", err) + } + log.Println("name=", name) +} diff --git a/src/database/sql/example_service_test.go b/src/database/sql/example_service_test.go new file mode 100644 index 00000000000..768307c1471 --- /dev/null +++ b/src/database/sql/example_service_test.go @@ -0,0 +1,158 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sql_test + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" +) + +func Example_openDBService() { + // Opening a driver typically will not attempt to connect to the database. + db, err := sql.Open("driver-name", "database=test1") + if err != nil { + // This will not be a connection error, but a DSN parse error or + // another initialization error. + log.Fatal(err) + } + db.SetConnMaxLifetime(0) + db.SetMaxIdleConns(50) + db.SetMaxOpenConns(50) + + s := &Service{db: db} + + http.ListenAndServe(":8080", s) +} + +type Service struct { + db *sql.DB +} + +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { + db := s.db + switch r.URL.Path { + default: + http.Error(w, "not found", http.StatusNotFound) + return + case "/healthz": + ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second) + defer cancel() + + err := s.db.PingContext(ctx) + if err != nil { + http.Error(w, fmt.Sprintf("db down: %v", err), http.StatusFailedDependency) + return + } + w.WriteHeader(http.StatusOK) + return + case "/quick-action": + // This is a short SELECT. Use the request context as the base of + // the context timeout. + ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) + defer cancel() + + id := 5 + org := 10 + var name string + err := db.QueryRowContext(ctx, ` +select + p.name +from + people as p + join organization as o on p.organization = o.id +where + p.id = :id + and o.id = :org +;`, + sql.Named("id", id), + sql.Named("org", org), + ).Scan(&name) + if err != nil { + if err == sql.ErrNoRows { + http.Error(w, "not found", http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + io.WriteString(w, name) + return + case "/long-action": + // This is a long SELECT. Use the request context as the base of + // the context timeout, but give it some time to finish. If + // the client cancels before the query is done the query will also + // be canceled. + ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) + defer cancel() + + var names []string + rows, err := db.QueryContext(ctx, "select p.name from people as p where p.active = true;") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + for rows.Next() { + var name string + err = rows.Scan(&name) + if err != nil { + break + } + names = append(names, name) + } + // Check for errors during rows "Close". + // This may be more important if multiple statements are executed + // in a single batch and rows were written as well as read. + if closeErr := rows.Close(); closeErr != nil { + http.Error(w, closeErr.Error(), http.StatusInternalServerError) + return + } + + // Check for row scan error. + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Check for errors during row iteration. + if err = rows.Err(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(names) + return + case "/async-action": + // This action has side effects that we want to preserve + // even if the client cancels the HTTP request part way through. + // For this we do not use the http request context as a base for + // the timeout. + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var orderRef = "ABC123" + tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}) + _, err = tx.ExecContext(ctx, "stored_proc_name", orderRef) + + if err != nil { + tx.Rollback() + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + err = tx.Commit() + if err != nil { + http.Error(w, "action in unknown state, check state before attempting again", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + return + } +} diff --git a/src/database/sql/example_test.go b/src/database/sql/example_test.go index da938b071a1..6f9bd91276e 100644 --- a/src/database/sql/example_test.go +++ b/src/database/sql/example_test.go @@ -13,8 +13,10 @@ import ( "time" ) -var ctx = context.Background() -var db *sql.DB +var ( + ctx context.Context + db *sql.DB +) func ExampleDB_QueryContext() { age := 27 @@ -24,13 +26,25 @@ func ExampleDB_QueryContext() { } defer rows.Close() names := make([]string, 0) + for rows.Next() { var name string if err := rows.Scan(&name); err != nil { + // Check for a scan error. + // Query rows will be closed with defer. log.Fatal(err) } names = append(names, name) } + // If the database is being written to ensure to check for Close + // errors that may be returned from the driver. The query may + // encounter an auto-commit error and be forced to rollback changes. + rerr := rows.Close() + if rerr != nil { + log.Fatal(err) + } + + // Rows.Err will report the last error encountered by Rows.Scan. if err := rows.Err(); err != nil { log.Fatal(err) } @@ -44,11 +58,11 @@ func ExampleDB_QueryRowContext() { err := db.QueryRowContext(ctx, "SELECT username, created_at FROM users WHERE id=?", id).Scan(&username, &created) switch { case err == sql.ErrNoRows: - log.Printf("No user with id %d", id) + log.Printf("no user with id %d\n", id) case err != nil: - log.Fatal(err) + log.Fatalf("query error: %v\n", err) default: - fmt.Printf("Username is %s, account created on %s\n", username, created) + log.Printf("username is %q, account created on %s\n", username, created) } } @@ -63,7 +77,7 @@ func ExampleDB_ExecContext() { log.Fatal(err) } if rows != 1 { - panic(err) + log.Fatalf("expected to affect 1 row, affected %d", rows) } } @@ -104,10 +118,10 @@ from if err := rows.Scan(&id, &name); err != nil { log.Fatal(err) } - fmt.Printf("id %d name is %s\n", id, name) + log.Printf("id %d name is %s\n", id, name) } if !rows.NextResultSet() { - log.Fatal("expected more result sets", rows.Err()) + log.Fatalf("expected more result sets: %v", rows.Err()) } var roleMap = map[int64]string{ 1: "user", @@ -122,7 +136,7 @@ from if err := rows.Scan(&id, &role); err != nil { log.Fatal(err) } - fmt.Printf("id %d has role %s\n", id, roleMap[role]) + log.Printf("id %d has role %s\n", id, roleMap[role]) } if err := rows.Err(); err != nil { log.Fatal(err) @@ -130,11 +144,23 @@ from } func ExampleDB_PingContext() { + // Ping and PingContext may be used to determine if communication with + // the database server is still possible. + // + // When used in a command line application Ping may be used to establish + // that further queries are possible; that the provided DSN is valid. + // + // When used in long running service Ping may be part of the health + // checking system. + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() + + status := "up" if err := db.PingContext(ctx); err != nil { - log.Fatal(err) + status = "down" } + log.Println(status) } func ExampleConn_BeginTx() { @@ -162,7 +188,7 @@ func ExampleConn_ExecContext() { } defer conn.Close() // Return the connection to the pool. id := 41 - result, err := conn.ExecContext(ctx, `UPDATE balances SET balance = balance + 10 WHERE user_id = ?`, id) + result, err := conn.ExecContext(ctx, `UPDATE balances SET balance = balance + 10 WHERE user_id = ?;`, id) if err != nil { log.Fatal(err) } @@ -171,7 +197,7 @@ func ExampleConn_ExecContext() { log.Fatal(err) } if rows != 1 { - panic(err) + log.Fatalf("expected single row affected, got %d rows affected", rows) } } @@ -184,9 +210,9 @@ func ExampleTx_ExecContext() { _, execErr := tx.ExecContext(ctx, "UPDATE users SET status = ? WHERE id = ?", "paid", id) if execErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { - log.Printf("Could not roll back: %v\n", rollbackErr) + log.Fatalf("update failed: %v, unable to rollback: %v\n", execErr, rollbackErr) } - log.Fatal(execErr) + log.Fatalf("update failed: %v", execErr) } if err := tx.Commit(); err != nil { log.Fatal(err) @@ -199,17 +225,17 @@ func ExampleTx_Rollback() { log.Fatal(err) } id := 53 - _, err = tx.ExecContext(ctx, "UPDATE drivers SET status = ? WHERE id = ?", "assigned", id) + _, err = tx.ExecContext(ctx, "UPDATE drivers SET status = ? WHERE id = ?;", "assigned", id) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { - log.Printf("Could not roll back: %v\n", rollbackErr) + log.Fatalf("update drivers: unable to rollback: %v", rollbackErr) } log.Fatal(err) } - _, err = tx.ExecContext(ctx, "UPDATE pickups SET driver_id = $1", id) + _, err = tx.ExecContext(ctx, "UPDATE pickups SET driver_id = $1;", id) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { - log.Printf("Could not roll back: %v\n", rollbackErr) + log.Fatalf("update failed: %v, unable to back: %v", err, rollbackErr) } log.Fatal(err) } @@ -225,17 +251,18 @@ func ExampleStmt() { log.Fatal(err) } defer stmt.Close() + // Then reuse it each time you need to issue the query. id := 43 var username string err = stmt.QueryRowContext(ctx, id).Scan(&username) switch { case err == sql.ErrNoRows: - log.Printf("No user with that ID.") + log.Fatalf("no user with id %d", id) case err != nil: log.Fatal(err) default: - fmt.Printf("Username is %s\n", username) + log.Printf("username is %s\n", username) } } @@ -245,17 +272,19 @@ func ExampleStmt_QueryRowContext() { if err != nil { log.Fatal(err) } + defer stmt.Close() + // Then reuse it each time you need to issue the query. id := 43 var username string err = stmt.QueryRowContext(ctx, id).Scan(&username) switch { case err == sql.ErrNoRows: - log.Printf("No user with that ID.") + log.Fatalf("no user with id %d", id) case err != nil: log.Fatal(err) default: - fmt.Printf("Username is %s\n", username) + log.Printf("username is %s\n", username) } } @@ -266,6 +295,7 @@ func ExampleRows() { log.Fatal(err) } defer rows.Close() + names := make([]string, 0) for rows.Next() { var name string @@ -274,8 +304,9 @@ func ExampleRows() { } names = append(names, name) } + // Check for errors from iterating over rows. if err := rows.Err(); err != nil { log.Fatal(err) } - fmt.Printf("%s are %d years old", strings.Join(names, ", "), age) + log.Printf("%s are %d years old", strings.Join(names, ", "), age) }