Documentation
¶
Overview ¶
Package bisql is a two-way SQL template engine for Go.
Directives are written as SQL comments, so a template is simultaneously a valid SQL statement — it can be pasted into a client and run as-is — while an application converts the same text into a parameterized statement (SQL, Args). The directive syntax is inspired by Komapper's TEMPLATE API.
bisql follows an explicit model: the renderer emits the template verbatim, evaluating only the bind, literal, conditional, iteration, and @include directives and stripping parser comments. It removes nothing implicitly — no empty-clause removal, no dangling AND/OR cleanup, no whitespace normalization — so the author anchors every dynamic fragment (a 1 = 1 predicate, a leading connector) to keep the rendered SQL valid. See the README for the directive reference and authoring rules.
Example ¶
A bind directive /* expr */ becomes a placeholder, replacing the trailing literal (the two-way sample value, ignored at build time). The template is valid SQL on its own; bisql turns it into a parameterized statement. SQLWithArgs is the values-embedded rendering, for review only — never execute it.
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
tmpl, err := bisql.Parse("select id from users where name = /*name*/'sample'")
if err != nil {
panic(err)
}
stmt, err := tmpl.Build(map[string]any{"name": "Alice"})
if err != nil {
panic(err)
}
fmt.Println(stmt.SQL)
fmt.Println(stmt.Args)
fmt.Println(stmt.SQLWithArgs())
}
Output: select id from users where name = ? [Alice] select id from users where name = 'Alice'
Example (ArrayBind) ¶
A scalar test binds a slice as ONE array parameter — the form PostgreSQL's = ANY needs — so the placeholder count is fixed regardless of the slice length. The values-embedded form shows it as a PostgreSQL array literal.
package main
import (
"fmt"
"github.com/mpyw/bisql"
"github.com/mpyw/bisql/dialect"
)
func main() {
tmpl, _ := bisql.Parse(
"select id from users where department_id = ANY(/*ids*/'{}'::int[])",
bisql.WithDialect(dialect.PostgreSQL),
)
stmt, _ := tmpl.Build(map[string]any{"ids": []int{1, 2, 3}})
fmt.Println(stmt.SQL)
fmt.Println(stmt.Args)
fmt.Println(stmt.SQLWithArgs())
}
Output: select id from users where department_id = ANY($1::int[]) [[1 2 3]] select id from users where department_id = ANY('{1,2,3}'::int[])
Example (Conditional) ¶
/*%if*/ … /*%elseif*/ … /*%else*/ … /*%end*/ renders the first branch whose condition is true. The engine removes nothing implicitly, so a dynamic WHERE is anchored with 1 = 1 and each branch leads with its own "and". (These branches bind nothing, so only the SQL varies.)
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
const t = "select id from users where 1 = 1 " +
"/*%if band == 'adult'*/and age >= 18" +
"/*%elseif band == 'senior'*/and age >= 65" +
"/*%else*/and age >= 0/*%end*/"
tmpl, _ := bisql.Parse(t)
for _, band := range []string{"adult", "senior", "child"} {
stmt, _ := tmpl.Build(map[string]any{"band": band})
fmt.Println(stmt.SQL)
}
}
Output: select id from users where 1 = 1 and age >= 18 select id from users where 1 = 1 and age >= 65 select id from users where 1 = 1 and age >= 0
Example (Dialects) ¶
The dialect only changes the placeholder spelling, never the arguments; the same template renders under each. (The values-embedded form is dialect-independent here, so only the parameterized SQL is shown; see Example_arrayBind for SQLWithArgs.)
package main
import (
"fmt"
"github.com/mpyw/bisql"
"github.com/mpyw/bisql/dialect"
)
func main() {
const t = "select id from users where name = /*name*/'x' and age >= /*age*/0"
for _, d := range []dialect.Dialect{dialect.MySQL, dialect.PostgreSQL, dialect.Oracle, dialect.SQLServer} {
tmpl, _ := bisql.Parse(t, bisql.WithDialect(d))
stmt, _ := tmpl.Build(map[string]any{"name": "Alice", "age": 18})
fmt.Printf("%s: %s\n", d.Name(), stmt.SQL)
}
}
Output: mysql: select id from users where name = ? and age >= ? postgresql: select id from users where name = $1 and age >= $2 oracle: select id from users where name = :1 and age >= :2 sqlserver: select id from users where name = @p1 and age >= @p2
Example (InListExpansion) ¶
A parenthesized test /*ids*/(...) expands an iterable into a placeholder list, so one bind covers a whole IN clause.
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
tmpl, _ := bisql.Parse("select id from users where department_id in /*ids*/(0)")
stmt, _ := tmpl.Build(map[string]any{"ids": []any{1, 2, 3}})
fmt.Println(stmt.SQL)
fmt.Println(stmt.Args)
fmt.Println(stmt.SQLWithArgs())
}
Output: select id from users where department_id in (?, ?, ?) [1 2 3] select id from users where department_id in (1, 2, 3)
Example (Include) ¶
@include composes a reusable fragment before parsing. Because the directive rides the parser-comment channel, the base statement still runs verbatim in a client without it.
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
ld := bisql.NewRegistryLoader().Register("active_filter", "and status = /*status*/'active'")
tmpl, _ := bisql.Parse(
"select id from users where 1 = 1 /*%! @include active_filter */",
bisql.WithLoader(ld),
)
stmt, _ := tmpl.Build(map[string]any{"status": "active"})
fmt.Println(stmt.SQL)
fmt.Println(stmt.Args)
fmt.Println(stmt.SQLWithArgs())
}
Output: select id from users where 1 = 1 and status = ? [active] select id from users where 1 = 1 and status = 'active'
Example (Iteration) ¶
/*%for*/ repeats its body with nothing inserted between iterations, so a list is kept two-way by anchoring it (here 1 = 0) and having each iteration lead with its own connector — note the leading space inside the body so consecutive iterations do not run together.
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
tmpl, _ := bisql.Parse(
"select id from users where 1 = 0" +
"/*%for kw in keywords*/ or name like /*kw*/'%x%'/*%end*/",
)
stmt, _ := tmpl.Build(map[string]any{"keywords": []any{"%a%", "%b%"}})
fmt.Println(stmt.SQL)
fmt.Println(stmt.Args)
fmt.Println(stmt.SQLWithArgs())
}
Output: select id from users where 1 = 0 or name like ? or name like ? [%a% %b%] select id from users where 1 = 0 or name like '%a%' or name like '%b%'
Example (Literal) ¶
/*^ expr */ inlines the value as a formatted SQL literal instead of binding it — for trusted values that cannot be parameterized. It is injection-prone. (No bind, so Args is empty.)
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
tmpl, _ := bisql.Parse("select id from users limit /*^limit*/10")
stmt, _ := tmpl.Build(map[string]any{"limit": 50})
fmt.Println(stmt.SQL)
fmt.Println(stmt.Args)
}
Output: select id from users limit 50 []
Index ¶
Examples ¶
- Package
- Package (ArrayBind)
- Package (Conditional)
- Package (Dialects)
- Package (InListExpansion)
- Package (Include)
- Package (Iteration)
- Package (Literal)
- Expand
- ExpandFile
- LoaderFunc
- NewFSLoader
- NewParser
- NewRegistryLoader
- NewStackedLoader
- Parse
- ParseFile
- Parser.Parse
- Parser.ParseFile
- RegistryLoader.Register
- Statement.SQLWithArgs
- Template.Build
- WithDialect
- WithEvaluator
- WithLoader
- WithStackedLoader
Constants ¶
This section is empty.
Variables ¶
var ErrNotFound = errors.New("bisql: @include fragment not found")
ErrNotFound reports that a Loader does not have the requested fragment. A Loader signals a missing fragment (as opposed to a genuine failure such as an I/O error) by returning an error e for which errors.Is(e, ErrNotFound) holds; a StackedLoader then falls through to the next loader. FSLoader reports a missing file with fs.ErrNotExist, which a StackedLoader also treats as "not found".
Functions ¶
func Expand ¶
Expand is a shortcut for NewParser(opts...).Expand(src).
Example ¶
Expand performs only the @include step and returns the resulting template text, leaving every other directive (here the /*status*/ bind) intact — so the result is still two-way.
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
ld := bisql.NewRegistryLoader().Register("active", "and status = /*status*/'active'")
expanded, err := bisql.Expand(
"select id from users where 1 = 1 /*%! @include active */",
bisql.WithLoader(ld),
)
if err != nil {
panic(err)
}
fmt.Println(expanded)
}
Output: select id from users where 1 = 1 and status = /*status*/'active'
func ExpandFile ¶
ExpandFile is a shortcut for NewParser(opts...).ExpandFile(fsys, name).
Example ¶
ExpandFile resolves @include from an fs.FS and returns the expanded, still-two-way text — useful for committing snapshots or inspecting with EXPLAIN.
package main
import (
"fmt"
"testing/fstest"
"github.com/mpyw/bisql"
)
func main() {
fsys := fstest.MapFS{
"report.sql": {Data: []byte("select count(*) from users /*%! @include _scope.sql */")},
"_scope.sql": {Data: []byte("where status = /*status*/'active'")},
}
expanded, err := bisql.ExpandFile(fsys, "report.sql")
if err != nil {
panic(err)
}
fmt.Println(expanded)
}
Output: select count(*) from users where status = /*status*/'active'
Types ¶
type FSLoader ¶
type FSLoader struct {
// contains filtered or unexported fields
}
FSLoader loads fragments from an fs.FS (e.g. embed.FS, os.DirFS). The @include name is the file's path within the FS; the extension is part of the name (e.g. @include sql/active.sql).
func NewFSLoader ¶
NewFSLoader creates an FSLoader over fsys.
Example ¶
NewFSLoader resolves @include names as file paths in an fs.FS.
package main
import (
"fmt"
"testing/fstest"
"github.com/mpyw/bisql"
)
func main() {
fsys := fstest.MapFS{
"_scope.sql": {Data: []byte("and status = /*status*/'active'")},
}
ld := bisql.NewFSLoader(fsys)
tmpl, _ := bisql.Parse(
"select id from users where 1 = 1 /*%! @include _scope.sql */",
bisql.WithLoader(ld),
)
stmt, _ := tmpl.Build(map[string]any{"status": "active"})
fmt.Println(stmt.SQL)
fmt.Println(stmt.Args)
fmt.Println(stmt.SQLWithArgs())
}
Output: select id from users where 1 = 1 and status = ? [active] select id from users where 1 = 1 and status = 'active'
type Loader ¶
Loader resolves an @include fragment name to its raw template text. Implement it to load fragments from anywhere (a DB table, a remote store, a cache, ...). bisql ships three implementations, RegistryLoader (in-memory), FSLoader (fs.FS), and StackedLoader (a chain); there is no default — pass one with WithLoader when a template uses /*%! @include ... */.
type LoaderFunc ¶
LoaderFunc adapts a function to Loader.
Example ¶
LoaderFunc adapts a plain resolver function to the Loader interface. (The fragment binds nothing, so only the SQL is shown.)
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
ld := bisql.LoaderFunc(func(name string) (string, error) {
return "and " + name + " = 1", nil
})
tmpl, _ := bisql.Parse("select id from users where 1 = 1 /*%! @include is_active */", bisql.WithLoader(ld))
stmt, _ := tmpl.Build(nil)
fmt.Println(stmt.SQL)
}
Output: select id from users where 1 = 1 and is_active = 1
type Option ¶
type Option func(*config)
Option adjusts Parse / Expand.
func WithDialect ¶
WithDialect sets the dialect used for placeholder generation (default: MySQL).
Example ¶
WithDialect selects the placeholder style (and literal formatting). (Its point is the placeholder spelling; the values-embedded form is the same for both, so only SQL is shown.)
package main
import (
"fmt"
"github.com/mpyw/bisql"
"github.com/mpyw/bisql/dialect"
)
func main() {
const t = "select id from users where id = /*id*/0"
my, _ := bisql.Parse(t, bisql.WithDialect(dialect.MySQL))
pg, _ := bisql.Parse(t, bisql.WithDialect(dialect.PostgreSQL))
m, _ := my.Build(map[string]any{"id": 1})
p, _ := pg.Build(map[string]any{"id": 1})
fmt.Println(m.SQL)
fmt.Println(p.SQL)
}
Output: select id from users where id = ? select id from users where id = $1
func WithEvaluator ¶
WithEvaluator swaps the expression evaluator (default: the built-in one).
Example ¶
WithEvaluator replaces the default expr-lang evaluator with a custom one.
package main
import (
"fmt"
"github.com/mpyw/bisql"
"github.com/mpyw/bisql/expr"
)
// identEvaluator is a minimal expr.Evaluator that resolves an expression as a bare scope key.
type identEvaluator struct{}
func (identEvaluator) Eval(expression string, scope expr.Scope) (any, error) {
return scope[expression], nil
}
func main() {
tmpl, _ := bisql.Parse(
"select id from users where 1 = 1 /*%if active*/and name = /*name*/'x'/*%end*/",
bisql.WithEvaluator(identEvaluator{}),
)
stmt, _ := tmpl.Build(map[string]any{"active": true, "name": "Alice"})
fmt.Println(stmt.SQL)
fmt.Println(stmt.Args)
fmt.Println(stmt.SQLWithArgs())
}
Output: select id from users where 1 = 1 and name = ? [Alice] select id from users where 1 = 1 and name = 'Alice'
func WithLoader ¶
WithLoader sets how /*%! @include name */ directives are resolved. There is no default: a template that uses @include must be parsed with a Loader (RegistryLoader, FSLoader, a LoaderFunc, or your own), otherwise @include is an error.
Example ¶
WithLoader supplies the Loader that resolves @include fragments.
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
ld := bisql.NewRegistryLoader().Register("recent", "and created_at >= /*since*/'2025-01-01'")
tmpl, _ := bisql.Parse(
"select id from audit_logs where 1 = 1 /*%! @include recent */",
bisql.WithLoader(ld),
)
stmt, _ := tmpl.Build(map[string]any{"since": "2025-06-01"})
fmt.Println(stmt.SQL)
fmt.Println(stmt.Args)
fmt.Println(stmt.SQLWithArgs())
}
Output: select id from audit_logs where 1 = 1 and created_at >= ? [2025-06-01] select id from audit_logs where 1 = 1 and created_at >= '2025-06-01'
func WithStackedLoader ¶
WithStackedLoader resolves fragments by trying loaders in order, falling through to the next whenever one reports the fragment is not found (see ErrNotFound); any other error aborts. It is shorthand for WithLoader(NewStackedLoader(loaders...)).
Example ¶
WithStackedLoader consults loaders in order, falling through when one reports the fragment is not found. When no loader has it, resolution fails with an error that is ErrNotFound. (The fragment here binds nothing, so only the SQL is shown.)
package main
import (
"errors"
"fmt"
"github.com/mpyw/bisql"
)
func main() {
base := bisql.NewRegistryLoader().Register("scope", "and status = 'active'")
// The empty override has no "scope", so the base loader supplies it.
tmpl, _ := bisql.Parse(
"select id from users where 1 = 1 /*%! @include scope */",
bisql.WithStackedLoader(bisql.NewRegistryLoader(), base),
)
stmt, _ := tmpl.Build(nil)
fmt.Println(stmt.SQL)
_, err := bisql.Parse(
"select 1 /*%! @include missing */",
bisql.WithStackedLoader(bisql.NewRegistryLoader()),
)
fmt.Println(errors.Is(err, bisql.ErrNotFound))
}
Output: select id from users where 1 = 1 and status = 'active' true
type Parser ¶
type Parser struct {
// contains filtered or unexported fields
}
Parser holds parse-time configuration (dialect, evaluator, loader) so it can be built once with NewParser and reused to parse many templates without repeating options. It is immutable and safe for concurrent use.
func NewParser ¶
NewParser returns a Parser configured by opts (dialect, evaluator, loader).
Example ¶
NewParser builds a Parser once; it is immutable and safe for concurrent use, so it is reused across every template rather than reconstructed per call.
package main
import (
"fmt"
"github.com/mpyw/bisql"
"github.com/mpyw/bisql/dialect"
)
func main() {
p := bisql.NewParser(bisql.WithDialect(dialect.PostgreSQL))
users, _ := p.Parse("select id from users where id = /*id*/0")
depts, _ := p.Parse("select id from departments where id = /*id*/0")
u, _ := users.Build(map[string]any{"id": 1})
fmt.Println(u.SQL)
fmt.Println(u.Args)
fmt.Println(u.SQLWithArgs())
d, _ := depts.Build(map[string]any{"id": 2})
fmt.Println(d.SQL)
fmt.Println(d.Args)
fmt.Println(d.SQLWithArgs())
}
Output: select id from users where id = $1 [1] select id from users where id = 1 select id from departments where id = $1 [2] select id from departments where id = 2
func (*Parser) Expand ¶
Expand runs only the @include preprocessor and returns the fully expanded, still-2-way template text (useful to snapshot or run through EXPLAIN ahead of time).
func (*Parser) ExpandFile ¶
ExpandFile reads the template named by name from fsys and returns its expanded text (see Expand and ParseFile).
func (*Parser) Parse ¶
Parse parses a template string, expanding any /*%! @include ... */ against the parser's loader (absent a loader, @include is an error).
Example ¶
Parser.Parse compiles a template string with the parser's configuration.
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
p := bisql.NewParser()
tmpl, err := p.Parse("select id from users where name = /*name*/'x'")
if err != nil {
panic(err)
}
stmt, _ := tmpl.Build(map[string]any{"name": "Alice"})
fmt.Println(stmt.SQL)
fmt.Println(stmt.Args)
fmt.Println(stmt.SQLWithArgs())
}
Output: select id from users where name = ? [Alice] select id from users where name = 'Alice'
func (*Parser) ParseFile ¶
ParseFile reads the template named by name from fsys and parses it. Unless the parser was configured with an explicit loader, /*%! @include ... */ directives are resolved from the same fsys (as an FSLoader), so the root template and its fragments live together in one file tree; include names are paths relative to the root of fsys.
Example ¶
Parser.ParseFile reads the root template from an fs.FS with the parser's configuration.
package main
import (
"fmt"
"testing/fstest"
"github.com/mpyw/bisql"
"github.com/mpyw/bisql/dialect"
)
func main() {
p := bisql.NewParser(bisql.WithDialect(dialect.PostgreSQL))
fsys := fstest.MapFS{
"q.sql": {Data: []byte("select id from users where id = /*id*/0")},
}
tmpl, err := p.ParseFile(fsys, "q.sql")
if err != nil {
panic(err)
}
stmt, _ := tmpl.Build(map[string]any{"id": 7})
fmt.Println(stmt.SQL)
fmt.Println(stmt.Args)
fmt.Println(stmt.SQLWithArgs())
}
Output: select id from users where id = $1 [7] select id from users where id = 7
type RegistryLoader ¶
type RegistryLoader struct {
// contains filtered or unexported fields
}
RegistryLoader is an in-memory Loader: fragments are registered by name.
func NewRegistryLoader ¶
func NewRegistryLoader() *RegistryLoader
NewRegistryLoader creates an empty RegistryLoader.
Example ¶
NewRegistryLoader holds fragments in memory; Register chains, so several are added fluently. (These fragments bind nothing, so only the SQL is shown.)
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
ld := bisql.NewRegistryLoader().
Register("active", "and status = 'active'").
Register("adult", "and age >= 18")
tmpl, _ := bisql.Parse(
"select id from users where 1 = 1 /*%! @include active */ /*%! @include adult */",
bisql.WithLoader(ld),
)
stmt, _ := tmpl.Build(nil)
fmt.Println(stmt.SQL)
}
Output: select id from users where 1 = 1 and status = 'active' and age >= 18
func (*RegistryLoader) Load ¶
func (r *RegistryLoader) Load(name string) (string, error)
Load implements Loader. An unregistered name returns an error satisfying ErrNotFound.
func (*RegistryLoader) Register ¶
func (r *RegistryLoader) Register(name, template string) *RegistryLoader
Register adds (or replaces) a named fragment and returns the loader for chaining.
Example ¶
RegistryLoader.Register adds a fragment and returns the loader, so calls chain.
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
ld := bisql.NewRegistryLoader().Register("scope", "and status = /*status*/'active'")
tmpl, _ := bisql.Parse("select id from users where 1 = 1 /*%! @include scope */", bisql.WithLoader(ld))
stmt, _ := tmpl.Build(map[string]any{"status": "banned"})
fmt.Println(stmt.SQL)
fmt.Println(stmt.Args)
fmt.Println(stmt.SQLWithArgs())
}
Output: select id from users where 1 = 1 and status = ? [banned] select id from users where 1 = 1 and status = 'banned'
type StackedLoader ¶
type StackedLoader struct {
// contains filtered or unexported fields
}
StackedLoader resolves a fragment by trying its loaders in order. It falls through to the next loader whenever one reports the fragment is not found (errors.Is(err, ErrNotFound), or fs.ErrNotExist); any other error aborts the lookup immediately. If no loader has the fragment, Load returns an error satisfying ErrNotFound, so stacks compose.
func NewStackedLoader ¶
func NewStackedLoader(loaders ...Loader) *StackedLoader
NewStackedLoader creates a StackedLoader over loaders, tried in the given order.
Example ¶
NewStackedLoader builds the loader explicitly; WithStackedLoader is a shorthand for WithLoader(NewStackedLoader(...)). (The fragment binds nothing, so only the SQL is shown.)
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
env := bisql.NewRegistryLoader() // empty: falls through
base := bisql.NewRegistryLoader().Register("scope", "and status = 'active'")
ld := bisql.NewStackedLoader(env, base)
tmpl, _ := bisql.Parse("select id from users where 1 = 1 /*%! @include scope */", bisql.WithLoader(ld))
stmt, _ := tmpl.Build(nil)
fmt.Println(stmt.SQL)
}
Output: select id from users where 1 = 1 and status = 'active'
type Statement ¶
Statement is the result of Build.
- SQL: placeholder form (for execution)
- Args: bind arguments
The values-embedded form is available via the SQLWithArgs method (computed on demand).
func (Statement) SQLWithArgs ¶
SQLWithArgs returns the values-embedded form of the statement — Args inlined as SQL literals — for snapshots and review. Never execute it: literals are not a substitute for bound parameters (injection). It is computed on demand from Args, so a Statement you only execute never pays for it; formatting is best-effort (a value the dialect cannot format falls back to Go's %v).
Example ¶
Statement.SQLWithArgs returns the values-embedded rendering, for review and snapshots only — never execute it. It is computed on demand from Args.
package main
import (
"fmt"
"github.com/mpyw/bisql"
"github.com/mpyw/bisql/dialect"
)
func main() {
tmpl, _ := bisql.Parse(
"select id from users where name = /*name*/'x' and age >= /*age*/0",
bisql.WithDialect(dialect.PostgreSQL),
)
stmt, _ := tmpl.Build(map[string]any{"name": "Alice", "age": 20})
fmt.Println(stmt.SQL) // execute this
fmt.Println(stmt.Args) // the bind arguments
fmt.Println(stmt.SQLWithArgs()) // review only; never execute
}
Output: select id from users where name = $1 and age >= $2 [Alice 20] select id from users where name = 'Alice' and age >= 20
type Template ¶
type Template struct {
// contains filtered or unexported fields
}
Template is a parsed template. It is immutable and safe for concurrent Build calls.
func Parse ¶
Parse is a shortcut for NewParser(opts...).Parse(src); use NewParser to reuse one configuration across many templates.
Example ¶
Parse compiles a template string into a reusable *Template.
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
tmpl, err := bisql.Parse("select id from users where department_id = /*dept*/0")
if err != nil {
panic(err)
}
stmt, _ := tmpl.Build(map[string]any{"dept": 3})
fmt.Println(stmt.SQL)
fmt.Println(stmt.Args)
fmt.Println(stmt.SQLWithArgs())
}
Output: select id from users where department_id = ? [3] select id from users where department_id = 3
func ParseFile ¶
ParseFile is a shortcut for NewParser(opts...).ParseFile(fsys, name).
Example ¶
ParseFile reads the root template from an fs.FS (for example an embed.FS) and, unless a loader is configured, resolves @include fragments from the same fs.FS.
package main
import (
"fmt"
"testing/fstest"
"github.com/mpyw/bisql"
)
func main() {
fsys := fstest.MapFS{
"users/by_status.sql": {Data: []byte("select id from users where 1 = 1 /*%! @include users/_active.sql */")},
"users/_active.sql": {Data: []byte("and status = /*status*/'active'")},
}
tmpl, err := bisql.ParseFile(fsys, "users/by_status.sql")
if err != nil {
panic(err)
}
stmt, _ := tmpl.Build(map[string]any{"status": "active"})
fmt.Println(stmt.SQL)
fmt.Println(stmt.Args)
fmt.Println(stmt.SQLWithArgs())
}
Output: select id from users where 1 = 1 and status = ? [active] select id from users where 1 = 1 and status = 'active'
func (*Template) Build ¶
Build assembles (SQL, Args) from the given parameters, which may be a map[string]any, an expr.Scope, or a struct.
Example ¶
Template.Build applies parameters, evaluating the directives into (SQL, Args). A parsed Template is immutable, so it is built repeatedly with different parameters. (The "without" case binds nothing.)
package main
import (
"fmt"
"github.com/mpyw/bisql"
)
func main() {
tmpl, _ := bisql.Parse("select id from users where 1 = 1 /*%if name != null*/and name = /*name*/'x'/*%end*/")
with, _ := tmpl.Build(map[string]any{"name": "Alice"})
fmt.Printf("%q %v\n", with.SQL, with.Args)
fmt.Println(with.SQLWithArgs())
without, _ := tmpl.Build(map[string]any{})
fmt.Printf("%q %v\n", without.SQL, without.Args)
}
Output: "select id from users where 1 = 1 and name = ?" [Alice] select id from users where 1 = 1 and name = 'Alice' "select id from users where 1 = 1 " []
Directories
¶
| Path | Synopsis |
|---|---|
|
Package dialect abstracts per-RDBMS placeholder generation and literal formatting (for the /*^ */ literal directive).
|
Package dialect abstracts per-RDBMS placeholder generation and literal formatting (for the /*^ */ literal directive). |
|
Package expr defines the pluggable evaluator for expressions inside directives (e.g.
|
Package expr defines the pluggable evaluator for expressions inside directives (e.g. |
|
internal
|
|
|
exprlang
Package exprlang is bisql's built-in expression evaluator for directive expressions (the e in /*%if e*/, the bind expression in /* e */, and so on).
|
Package exprlang is bisql's built-in expression evaluator for directive expressions (the e in /*%if e*/, the bind expression in /* e */, and so on). |
|
sqltmpl/ast
Package ast is the template tree.
|
Package ast is the template tree. |
|
sqltmpl/lexer
Package lexer scans a SQL template into tokens.
|
Package lexer scans a SQL template into tokens. |
|
sqltmpl/parser
Package parser builds the template tree from a SQL template using a reducer-stack strategy for the block directives (if/for) and the bind/literal test literals.
|
Package parser builds the template tree from a SQL template using a reducer-stack strategy for the block directives (if/for) and the bind/literal test literals. |
|
sqltmpl/preprocess
Package preprocess implements the @include preprocessor.
|
Package preprocess implements the @include preprocessor. |
|
sqltmpl/render
Package render evaluates the template tree into (SQL, args).
|
Package render evaluates the template tree into (SQL, args). |
|
sqltmpl/token
Package token defines the token kinds of the SQL template layer.
|
Package token defines the token kinds of the SQL template layer. |