package sqltemplate import ( "fmt" "regexp" "strings" "text/template" ) // This file contains runnable examples. They serve the purpose of providing // idiomatic usage of the package as well as showing how it actually works, // since the examples are actually run together with regular Go tests. Note that // the "Output" comment section at the end of each function starting with // "Example" is used by the standard Go test tool to check that the standard // output of the function matches the commented text until the end of the // function. If you change the function, you may need to adapt that comment // section as it's possible that the output changes, causing it to fail tests. // To learn more about Go's runnable tests, which are a core builtin feature of // Go's standard testing library, see: // https://pkg.go.dev/testing#hdr-Examples // // If you're unfamiliar with Go text templating language, please, consider // reading that library's documentation first. // In this example we will use both Args and Dialect to dynamically and securely // build SQL queries, while also keeping track of the arguments that need to be // passed to the database methods to replace the placeholder "?" with the // correct values. // We will start by assuming we receive a request to retrieve a user's // information and that we need to provide a certain response. type GetUserRequest struct { ID int } type GetUserResponse struct { ID int Type string Name string } // Our template will take care for us of taking the request to build the query, // and then sort the arguments for execution as well as preparing the values // that need to be read for the response. We wil create a struct to pass the // request and an empty response, as well as a *SQLTemplate that will provide // the methods to achieve our purpose:: type GetUserQuery struct { SQLTemplate Request *GetUserRequest Response *GetUserResponse } // And finally we will define our template, that is free to use all the power of // the Go templating language, plus the methods we added with *SQLTemplate: var getUserTmpl = template.Must(template.New("example").Parse(` SELECT {{ .Ident "id" | .Into .Response.ID }}, {{ .Ident "type" | .Into .Response.Type }}, {{ .Ident "name" | .Into .Response.Name }} FROM {{ .Ident "users" }} WHERE {{ .Ident "id" }} = {{ .Arg .Request.ID }}; `)) // There are three interesting methods used in the above template: // 1. Ident: safely escape a SQL identifier. Even though here the only // identifier that may be problematic is "type" (because it is a reserved // word in many dialects), it is a good practice to escape all identifiers // just to make sure we're accounting for all variability in dialects, and // also for consistency. // 2. Into: this causes the selected field to be saved to the corresponding // field of GetUserQuery. // 3. Arg: this allows us to state that at this point will be a "?" that has to // be populated with the value of the given field of GetUserQuery. func Example() { // Let's pretend this example function is the handler of the GetUser method // of our service to see how it all works together. queryData := &GetUserQuery{ // The dialect (in this case we chose MySQL) should be set in your // service at startup when you connect to your database SQLTemplate: New(MySQL), // This is a synthetic request for our test Request: &GetUserRequest{ ID: 1, }, // Create an empty response to be populated Response: new(GetUserResponse), } // The next step is to execute the query template for our queryData, and // generate the arguments for the db.QueryRow and row.Scan methods later query, err := Execute(getUserTmpl, queryData) if err != nil { panic(err) // terminate the runnable example on error } // Assuming that we have a *sql.DB object named "db", we could now make our // query with: // row := db.QueryRowContext(ctx, query, queryData.GetArgs()...) // // and check row.Err() here // As we're not actually running a database in this example, let's verify // that we find our arguments populated as expected instead: if len(queryData.GetArgs()) != 1 { panic(fmt.Sprintf("unexpected number of args: %#v", queryData.GetArgs()...)) } id, ok := queryData.GetArgs()[0].(int) if !ok || id != queryData.Request.ID { panic(fmt.Sprintf("unexpected args: %#v", queryData.GetArgs()...)) } // In your code you would now have "row" populated with the row data, // assuming that the operation succeeded, so you would now scan the row data // abd populate the values of our response: // err := row.Scan(queryData.GetScanDest()...) // // and check err here // Again, as we're not actually running a database in this example, we will // instead run the code to assert that queryData.ScanDest was populated with // the expected data, which should be pointers to each of the fields of // Response so that the Scan method can write to them: if len(queryData.GetScanDest()) != 3 { panic(fmt.Sprintf("unexpected number of scan dest: %#v", queryData.GetScanDest()...)) } idPtr, ok := queryData.GetScanDest()[0].(*int) if !ok || idPtr != &queryData.Response.ID { panic(fmt.Sprintf("unexpected response 'id' pointer: %#v", queryData.GetScanDest()...)) } typePtr, ok := queryData.GetScanDest()[1].(*string) if !ok || typePtr != &queryData.Response.Type { panic(fmt.Sprintf("unexpected response 'type' pointer: %#v", queryData.GetScanDest()...)) } namePtr, ok := queryData.GetScanDest()[2].(*string) if !ok || namePtr != &queryData.Response.Name { panic(fmt.Sprintf("unexpected response 'name' pointer: %#v", queryData.GetScanDest()...)) } // Remember the variable "query"? Well, we didn't check it. We will now make // use of Go's runnable examples and print its contents to standard output // so Go's tooling verify this example's output each time we run tests. // By the way, to make the result more stable, we will remove some // unnecessary white space from the query. whiteSpaceRE := regexp.MustCompile(`\s+`) query = strings.TrimSpace(whiteSpaceRE.ReplaceAllString(query, " ")) fmt.Println(query) // Output: // SELECT `id`, `type`, `name` FROM `users` WHERE `id` = ?; }