sqlt

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 22, 2025 License: Apache-2.0 Imports: 10 Imported by: 0

README

Sqlt

❯ SQL Templating

license last-commit repo-top-language repo-language-count Go Reference


Table of contents


Overview

Sqlt provides safe template-based SQL generation for Golang.

This library primarily exists to solve the human problem of code review for generated SQL queries. This allows users to write their SQL queries in SQL files, where CODEOWNERS scanning can easily route changes affecting SQL query generation to database SMEs who can then easily see the structure of the resulting queries without needing to sift through branches of Go code in which they may have less expertise reading.

It does so by using a wrapper around Golang's standard library text/template templating package that escapes templates by ensuring any Action Nodes-the parts of the template that render runtime data-end their pipeline with a safe SQL sanitizer function.

Users may provide their own bindings of SQL sanitizers and end action node pipelines with those custom SQL sanitizers. If a pipeline doesn't end with a registered sanitizer, this wrapper safely escapes the value by appending a default sanitizer that replaces the value with a named parameter binding.

Example

In the following example, the code parses a template from a string literal for simplicity. You may wish to use forbidigo or some other linter to forbid parsing templates from string literals and require developers to use the ParseFS parser, which loads templates from a filesystem-like object such as an embed.FS. This ensures that every line of SQL exists in a separate file.


tpl, err := New("q").Parse(`SELECT * FROM users WHERE id={{ .ID }}`)
if err != nil {
    panic(err)
}

query, args, err := tpl.Execute(map[string]any{"ID": 123})
if err != nil {
    panic(err)
}

pool, err := pgxpool.New(context.TODO(), os.Getenv("DATABASE_URL"))
if err != nil {
	panic(err)
}

rows, err := pool.Query(context.TODO(), query, args)
if err != nil {
	panic(err)
}

user, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[User])
if err != nil {
	panic(err)
}

// use User in your code.

Getting started

Prerequisites

Before getting started with sqlt, ensure your runtime environment meets the following requirements:

  • Programming Language: Go
  • Package Manager: Go modules

Installation

Install sqlt using one of the following methods:

Build from source:

  1. Clone the sqlt repository:
❯ git clone https://github.com/hxtk/sqlt
  1. Navigate to the project directory:
❯ cd sqlt
  1. Install the project dependencies:

Using go modules  

❯ go build

Testing

Run the test suite using the following command: Using go modules  

❯ go test ./...

Contributing

Contributor Graph

Contributing code

For code contributions, this repository follows the practices described in Google's engineering practices handbook. Note that in the standard of code review, the standard states that a reviewer should approve a CL if it offers a net improvement to the codebase, even if they wouldn't call it perfect. "A net improvement to the codebase" leaves room for maintainer discretion when it comes to feature additions. Contributors should open an issue first if they would like to know whether maintainers would accept a feature before they begin working on it.

Commit messages should use Conventional Commits.

Authors can expect code review comments to use Conventional Comments.

Authors shouldn't make new commits for code review fixups. In general, use the structure of commits to tell a story about the evolution of the library. Keep each commit reasonably small, and limit a pull request to a small number of them. A simple pull request should only include one commit, but you might submit a more complex change as a series of "red, green, refactor" commits typical of test-driven development. If you update your pull request in response to comments, do so by amending the original commit and force-pushing to your work branch.

Jujutsu provides a cleaner interface for authoring commits in the style the maintainers of this library prefer.

License

The authors distribute this project under the Apache 2.0 License. For more details, refer to the LICENSE file.

Acknowledgments

For the developer experience and ergonomics of developing database code, this project draws inspiration from https://github.com/sqlc-dev/sqlc. The maintainers of this repository thank the maintainers of sqlc for inspiring the workflow of authoring SQL code by writing SQL queries in separate files where SQL experts have an easier time reviewing them, repository management structures such as the CODEOWNERS file have an easier time ensuring that SQL changes actually get reviewed by a SQL expert, and code editors have an easier time correctly highlighting the syntax.

This project differentiates itslf from sqlc because it doesn't include a build pipeline stage and it supports more dynamic queries, possibly with user-provided SQL generators. This dynamicism comes at the expense of the query analysis techniques that sqlc provides during its build phase.

For its implementation, this project draws heavy inspiration from https://github.com/VauntDev/tqla. The maintainers thank @VauntDev for inspiring the use of escaped text/templates to generate SQL.

This project differentiates itself from tqla by aligning more with the base text/template library to support writing templates in SQL files and reduce duplicated parsing work. It also supports "bring-your-own" SQL sanitizers besides the default sanitizer, so that users may safely render, for example, WHERE clauses generated by a safe filter string parser.

Documentation

Overview

Package sqlt defines an extension over text/templates for producing safe SQL.

Like package `html/template`, this package parses normal text templates and then modifies the syntax tree to alter potentially unsafe components.

By default, any value interpolation gets a special internal formatter function applied to it that map the value to a named parameter to prevent SQL injection vulnerabilities.

Index

Constants

This section is empty.

Variables

View Source
var ErrAlreadyUsed = errors.New("cannot parse template after first use")

ErrAlreadyUsed occurs when a program calls Parse or ParseFS on a template after its first use.

Templates lazily escape template sequences on first use, so parsing more templates after first use would allow an undefined state where the template has unescaped nodes.

View Source
var ErrUnexpectedNode = errors.New("unexpected node while escaping template")

ErrUnexpectedNode indicates that the escaper failed to escape the template.

This error should never occur, and if it does, it indicates a bug in this package.

Functions

This section is empty.

Types

type Sanitizer

type Sanitizer interface {
	// Format must output a safe SQL string associated with an argument.
	//
	// A Sanitizer must convert any untrusted inputs into named parameters
	// and store the associated values for later retrieval via Args.
	//
	// A Template may invoke `Format` any number of times during its execution.
	Format(arg any) string

	// Args must output the collected parameter bindings associated with all
	// calls to `Format` before calling Args.
	//
	// A Template calls args after it finishes executing the template to
	// retrieve all bound parameters created in the course of template execution.
	Args() pgx.NamedArgs
}

Sanitizer formats data for safe SQL escaping.

This package provides a default formatter that converts variable or function pipelines in the template to parameter bindings by default.

Callers may define their own Sanitizers to support other functions that generate safe SQL and parameter mappings.

type SanitizerFunc

type SanitizerFunc func() Sanitizer

SanitizerFunc defines a function that returns a new instance of a Sanitizer.

A Template invokes a SanitizerFunc once per execution, and the SanitizerFunc must return "clean" instance of the Sanitizer each time.

The Template generates a new Sanitizer for each execution because the Sanitizer stores state related to the parameter bindings generated by the execution.

type SanitizerMap

type SanitizerMap map[string]SanitizerFunc

SanitizerMap represents a binding from sanitizers to the names by which templates may access them.

A Template doesn't automatically escape a function pipeline ending with a sanitizer, and assumes the sanitizer produces trusted output.

This package reserves the namespace prefix `_sqlt_*` for function names. Callers shouldn't define functions having that prefix in a FuncMap or a SanitizerMap.

type Template

type Template struct {
	// contains filtered or unexported fields
}

Template wraps `template.Template` providing SQL parameter substitution to avoid directly constructing SQL with string interpolation.

Branches get interpreted as normal, resulting in literal strings within the query, but variable substitutions instead bind named parameters, and executing the query returns a parameter map containing these named parameters.

func New

func New(name string) *Template

New produces a new template, analogous to `template.New`.

func (*Template) Clone

func (t *Template) Clone() (*Template, error)

Clone creates a deep copy of the template.

func (*Template) Execute

func (t *Template) Execute(data any) (query string, params pgx.NamedArgs, err error)

Execute executes the template and returns a query, a `pgx.NamedArgs` containing any parameter bindings generated by the template, and an error.

If the error has a nil value, this method guarantees the validity of the other retur values.

Example:

pool, err := pgxpool.New(context.TODO(), os.Getenv("DATABASE_URL"))
if err != nil {
	panic(err)
}

tpl, err := sqlt.New("")
if err != nil {
	panic(err)
}

tpl, err = tpl.Parse("SELECT * FROM table WHERE id={{ .id }}")
if err != nil {
	panic(err)
}

query, args, err := tpl.Execute(map[string]int{"id": 3}
if err != nil {
	panic(err)
}

rows, err := pool.Query(context.TODO(), query, args)

func (*Template) ExecuteTemplate

func (t *Template) ExecuteTemplate(name string, data any) (query string, params pgx.NamedArgs, err error)

ExecuteTemplate executes a named subtemplate within this template.

This method especially helps when you load templates using `ParseFS`. Other than the extra argument for the template name, its behavor matches the preceding Execute method.

func (*Template) Funcs

func (t *Template) Funcs(funcMap template.FuncMap) *Template

Funcs passes new functions into the `Funcs` of the underlying text/template.

func (*Template) Parse

func (t *Template) Parse(templateStr string) (*Template, error)

Parse loads a single template into the root of this template.

func (*Template) ParseFS

func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error)

ParseFS loads many templates from a file system interface.

func (*Template) Sanitizers

func (t *Template) Sanitizers(sanitizers SanitizerMap) *Template

Sanitizers extends the behavior of `Funcs` to support adding SQL sanitizing functions that emit safe SQL strings and generate parameter bindings.

The caller has the responsibility of ensuring that the parameter bindings generated by each Sanitizer don't have namespace conflicts with one another.

This package reserves the prefix "sqlt" for its own bindings.