# README
sqlt: A Go Template-Based SQL Builder and ORM
import "github.com/wroge/sqlt"
sqlt
uses Go’s template engine to create a flexible, powerful, and type-safe SQL builder and ORM.
Type-Safety without a Build Step
- Define SQL statements at the global level using functions like
New
,Parse
,ParseFiles
,ParseFS
,ParseGlob
,Funcs
andLookup
. - Templates are validated via jba/templatecheck during application startup.
- Execute statements using methods such as
Exec
,Query
orQueryRow
. - Execute query statements using
First
,One
orAll
. - Use
Scan
functions to map columns to struct fields (Scan
forsql.Scanner's
,ScanInt64
forint64
,ScanString
forstring
,ScanTime
fortime.Time
,ScanStringP
for*string
, etc.). - Single-column queries do not require
Scan
functions.
type Insert struct {
ID int64
Title string
}
var insertBooks = sqlt.Stmt[[]Insert](
sqlt.Parse(`
INSERT INTO books (id, title) VALUES
{{ range $i, $v := . }}
{{ if $i }}, {{ end }}
({{ $v.ID }}, {{ $v.Title }})
{{ end }}
RETURNING id;
`),
)
type Query struct {
Title string
}
type Book struct {
ID int64
Title string
}
var queryBooks = sqlt.QueryStmt[Query, Book](
sqlt.New("query_books"),
sqlt.Parse(`
SELECT
{{ ScanInt64 Dest.ID "id" }}
{{ ScanString Dest.Title ", title" }}
FROM books
WHERE title = {{ .Titel }};
`),
)
// panic: location: [/.../main.go:17]: template: query_books:6:19: checking "query_books" at <.Titel>: can't use field Titel in type main.Query
var queryID = sqlt.QueryStmt[string, int64](
sqlt.Parse(`SELECT id FROM books WHERE title = {{ . }};`),
)
result, err := insertBooks.Exec(ctx, db, []Insert{
{ID: 1, Title: "The Hobbit"},
{ID: 2, Title: "Harry Potter and the Philosopher's Stone"},
})
books, err := queryBooks.All(ctx, db, Query{Title: "The Hobbit"})
id, err := queryID.One(ctx, db, "The Hobbit")
Support for multiple Dialects and Placeholders
- Templates are escaped, ensuring the package is not vulnerable to SQL injection.
- You can use both static placeholders (
?
) and positional placeholders (Go format strings like%d
). - This package supports any template functions (like
lower
orfail
from Masterminds/sprig). - Multiple dialects can be used by implementing your own template functions.
var queryBooks = sqlt.QueryStmt[string, Book](
sqlt.Dollar(), // equivalent to sqlt.Placeholder("$%d")
sqlt.Funcs(sprig.TxtFuncMap()),
sqlt.Funcs(template.FuncMap{
"Dialect": func() string {
return "postgres"
},
}),
sqlt.Parse(`
SELECT
{{ ScanInt64 Dest.ID "id" }}
{{ ScanString Dest.Title ", title" }}
FROM books
WHERE
{{ if eq Dialect "sqlite" }}
INSTR(LOWER(title), {{ lower . }})
{{ else if eq Dialect "postgres" }}
POSITION({{ lower . }} IN LOWER(title)) > 0
{{ else }}
{{ fail "invalid dialect" }}
{{ end }};
`),
)
books, err := queryBooks.All(ctx, db, "The Hobbit")
// SELECT id, title FROM books WHERE POSITION($1 IN LOWER(title)) > 0; ["the hobbit"]
Outsourcing Options into a Config
- All options can be grouped into a configuration struct for reusability.
- The
Start
andEnd
functions enable monitoring and logging of SQL queries.
type StartTime struct{}
var config = sqlt.Config{
Placeholder: sqlt.Dollar(),
TemplateOptions: []sqlt.TemplateOption{
sqlt.Funcs(sprig.TxtFuncMap()),
sqlt.Funcs(template.FuncMap{
"Dialect": func() string {
return "postgres"
},
}),
},
Start: func(runner *sqlt.Runner) {
runner.Context = context.WithValue(runner.Context, StartTime{}, time.Now())
},
End: func(err error, runner *sqlt.Runner) {
fmt.Println("location=%s, sql=%s, duration=%s", runner.Location, runner.SQL, time.Since(runner.Context.Value(StartTime{}).(time.Time)))
},
}
var queryBooks = sqlt.QueryStmt[string, Book](
config,
sqlt.Parse(`
SELECT
{{ ScanInt64 Dest.ID "id" }}
{{ ScanString Dest.Title ", title" }}
FROM books
WHERE
{{ if eq Dialect "sqlite" }}
INSTR(LOWER(title), {{ lower . }})
{{ else if eq Dialect "postgres" }}
POSITION({{ lower . }} IN LOWER(title)) > 0
{{ else }}
{{ fail "invalid dialect" }}
{{ end }};
`),
)
Any more Questions?
- Take a look into my vertical-slice-architecture example project.
- The template escape function is adapted from mhilton/sqltemplate.
# Functions
AtP is a positional placeholder.
Colon is a positional placeholder.
Dollar is a positional placeholder.
Funcs is equivalent to the method from text/template.
InTx simplifies the execution of multiple queries in a transaction.
Lookup is equivalent to the method from text/template.
MissingKeyError is equivalent to the method 'Option("missingkey=error")' from text/template.
MissingKeyInvalid is equivalent to the method 'Option("missingkey=invalid")' from text/template.
MissingKeyZero is equivalent to the method 'Option("missingkey=zero")' from text/template.
New is equivalent to the method from text/template.
Parse is equivalent to the method from text/template.
ParseFiles is equivalent to the method from text/template.
ParseFS is equivalent to the method from text/template.
ParseGlob is equivalent to the method from text/template.
QueryStmt creates a type-safe QueryStatement using variadic options.
Question is a static placeholder.
Scan is a Scanner for values, that can be used directly with your sql driver.
ScanJSON is a Scanner to unmarshal byte strings into T.
Stmt creates a type-safe Statement using variadic options.
# Variables
ErrTooManyRows is returned from One, when there are more than one rows.
# Structs
Config groups the available options.
QueryRunner groups the relevant data for each 'run' of a QueryStatement.
QueryStatement is a QueryRunner pool and a type-safe sql query executor.
Runner groups the relevant data for each 'run' of a Statement.
A Scanner is used to map columns to struct fields.
SQL implements io.Writer and fmt.Stringer.
Statements is a Runner pool and a type-safe sql executor.
# Type aliases
End is executed when a Runner is put back into a statement pool.
Placeholder can be static or positional using a go-formatted string ('%d').
Raw is used to write strings directly into the sql output.
Start is executed when a Runner is returned from a statement pool.
TemplateOption can be used to configure the template of a statement.