@ -2,6 +2,7 @@ package sqltemplate
import (
import (
"fmt"
"fmt"
"regexp"
"strings"
"strings"
"text/template"
"text/template"
)
)
@ -17,105 +18,139 @@ import (
// To learn more about Go's runnable tests, which are a core builtin feature of
// To learn more about Go's runnable tests, which are a core builtin feature of
// Go's standard testing library, see:
// Go's standard testing library, see:
// https://pkg.go.dev/testing#hdr-Examples
// 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
// 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
// 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
// passed to the database methods to replace the placeholder "?" with the
// correct values. If you're not familiar with Go text templating language,
// correct values.
// please, consider reading that library's documentation first.
// We will start by assuming we receive a request to retrieve a user's
// We will start with creating a simple text template to insert a new row into a
// information and that we need to provide a certain response.
// users table:
var createUserTmpl = template . Must ( template . New ( "query" ) . Parse ( `
type GetUserRequest struct {
INSERT INTO users ( id , { { . Ident "type" } } , name )
ID int
VALUES ( { { . Arg . ID } } , { { . Arg . Type } } , { { . Arg . Name } } ) ;
}
` ) )
// The two interesting methods here are Arg and Ident. Note that now we have a
type GetUserResponse struct {
// reusable text template, that will dynamically create the SQL code when
// executed, which is interesting because we have a SQL-implementation dependant
// code handled for us within the template (escaping the reserved word "type"),
// but also because the arguments to the database Exec method will be handled
// for us. The struct with the data needed to create a new user could be
// something like the following:
type CreateUserRequest struct {
ID int
ID int
Name string
Type string
Type string
Name string
}
}
// Note that this struct could actually come from a different definition, for
// Our template will take care for us of taking the request to build the query,
// example, from a DTO. We can reuse this DTO and create a smaller struct for
// and then sort the arguments for execution as well as preparing the values
// the purpose of writing to the database without the need of mapping:
// that need to be read for the response. We wil create a struct to pass the
type DBCreateUserRequest struct {
// request and an empty response, as well as a *SQLTemplate that will provide
Dialect // provides access to all Dialect methods, like Ident
// the methods to achieve our purpose::
* Args // provides access to Arg method, to keep track of db arguments
* CreateUserRequest
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 ( ) {
func Example ( ) {
// Finally, we can take a request received from a user like the following:
// Let's pretend this example function is the handler of the GetUser method
dto := & CreateUserRequest {
// of our service to see how it all works together.
ID : 1 ,
Name : "root" ,
queryData := & GetUserQuery {
Type : "admin" ,
// 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 ,
} ,
// Put it into a database request:
// Create an empty response to be populated
req := DBCreateUserRequest {
Response : new ( GetUserResponse ) ,
Dialect : SQLite , // set at runtime, the template is agnostic
Args : new ( Args ) ,
CreateUserRequest : dto ,
}
}
// Then we finally execute the template to both generate the SQL code and to
// The next step is to execute the query template for our queryData, and
// populate req.Args with the arguments:
// generate the arguments for the db.QueryRow and row.Scan methods later
var b strings . Builder
query , err := Execute ( getUserTmpl , queryData )
err := createUserTmpl . Execute ( & b , req )
if err != nil {
if err != nil {
panic ( err ) // terminate the runnable example on error
panic ( err ) // terminate the runnable example on error
}
}
// And we should finally be able to see the SQL generated, as well as
// Assuming that we have a *sql.DB object named "db", we could now make our
// getting the arguments populated for execution in a database. To execute
// query with:
// it in the databse, we could run:
// row := db.QueryRowContext(ctx, query, queryData.Args...)
// db.ExecContext(ctx, b.String(), req.Args...)
// // and check row.Err() here
// To provide the runnable example with some code to test, we will now print
// As we're not actually running a database in this example, let's verify
// the values to standard output:
// that we find our arguments populated as expected instead:
fmt . Println ( b . String ( ) )
if len ( queryData . Args ) != 1 {
fmt . Printf ( "%#v" , req . Args )
panic ( fmt . Sprintf ( "unexpected number of args: %#v" , queryData . Args ) )
}
// Output:
id , ok := queryData . Args [ 0 ] . ( int )
// INSERT INTO users (id, "type", name)
if ! ok || id != queryData . Request . ID {
// VALUES (?, ?, ?);
panic ( fmt . Sprintf ( "unexpected args: %#v" , queryData . Args ) )
//
}
// &sqltemplate.Args{1, "admin", "root"}
}
// A more complex template example follows, which should be self-explanatory
// given the previous example. It is left as an exercise to the reader how the
// code should be implemented, based on the ExampleCreateUser function.
// List users example.
// In your code you would now have "row" populated with the row data,
var _ = template . Must ( template . New ( "query" ) . Parse ( `
// assuming that the operation succeeded, so you would now scan the row data
SELECT id , { { . Ident "type" } } , name
// abd populate the values of our response:
FROM users
// err := row.Scan(queryData.ScanDest...)
WHERE
// // and check err here
{ { if eq . By "type" } }
{ { . Ident "type" } } = { { . Arg . Value } }
// Again, as we're not actually running a database in this example, we will
{ { else if eq . By "name" } }
// instead run the code to assert that queryData.ScanDest was populated with
name LIKE { { . Arg . Value } }
// the expected data, which should be pointers to each of the fields of
{ { end } } ;
// Response so that the Scan method can write to them:
` ) )
if len ( queryData . ScanDest ) != 3 {
panic ( fmt . Sprintf ( "unexpected number of scan dest: %#v" , queryData . ScanDest ) )
}
idPtr , ok := queryData . ScanDest [ 0 ] . ( * int )
if ! ok || idPtr != & queryData . Response . ID {
panic ( fmt . Sprintf ( "unexpected response 'id' pointer: %#v" , queryData . ScanDest ) )
}
typePtr , ok := queryData . ScanDest [ 1 ] . ( * string )
if ! ok || typePtr != & queryData . Response . Type {
panic ( fmt . Sprintf ( "unexpected response 'type' pointer: %#v" , queryData . ScanDest ) )
}
namePtr , ok := queryData . ScanDest [ 2 ] . ( * string )
if ! ok || namePtr != & queryData . Response . Name {
panic ( fmt . Sprintf ( "unexpected response 'name' pointer: %#v" , queryData . ScanDest ) )
}
type ListUsersRequest struct {
// Remember the variable "query"? Well, we didn't check it. We will now make
By string
// use of Go's runnable examples and print its contents to standard output
Value string
// 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 )
type DBListUsersRequest struct {
// Output:
Dialect
// SELECT "id", "type", "name" FROM "users" WHERE "id" = ?;
* Args
ListUsersRequest
}
}