render

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: Apache-2.0 Imports: 3 Imported by: 0

README

render — text, placeholders and bound values

The bottom layer. It owns Dialect, Args, SQLer, Raw, Quote and ErrUnsupportedDialect, and it imports only the standard library.

It says nothing about SQL, and that is load-bearing. Operators, join types, sort directions and keywords are SQL vocabulary and belong to the layers above, because a Gremlin or MongoDB target has no JOIN and no <>. TestRender_ServesANonSQLTarget drives this layer with a non-SQL dialect and TestRender_ImportsOnlyStdlib pins the floor. A construct, operator or keyword added here is a defect even when it compiles.

Full documentation

The page for this package is docs/render/README.md, with runnable snippets — every one of them copied from a test whose expected output go test checks. The documentation index is ../docs/README.md and the repository README is ../README.md.

Documentation

Overview

Package render is the bottom of the builder: how a value becomes a bound parameter, how an identifier is escaped, and how the first error of a render is carried out.

Nothing here knows what SQL is. There are no operators, no join types and no keywords, because those are vocabulary rather than rendering and they live in the layers above. That absence is the point: any target whose wire form is text can build on this package, and a target that renders something other than text needs its own bottom layer rather than a dialect here.

It imports nothing outside the standard library and never will, because the next level down, an actual driver, is a third-party dependency by definition.

Index

Constants

View Source
const (
	DialectPostgres   = "postgres"
	DialectSQLite     = "sqlite"
	DialectClickHouse = "clickhouse"
	DialectMySQL      = "mysql"
	DialectUnknown    = "unknown"
)

The engine names this module ships.

Not a closed set. Nothing in the core branches on these, and DialectName asks the Named interface, so a dialect from another repository returning any string works exactly as these three do. They are constants because the engine packages here guard on them and an error message prints them, and a typo in either would be silent.

Variables

View Source
var ErrNoLiteralQuoting = errors.New("sqlb: dialect cannot write a literal")

ErrNoLiteralQuoting reports a literal requested of a dialect that does not implement LiteralQuoting.

View Source
var ErrNotInlinable = errors.New("sqlb: value cannot be written as a literal")

ErrNotInlinable reports a value a dialect will not write as a literal, because the dialect has no faithful spelling for its type or for this value.

View Source
var ErrUnsupportedDialect = errors.New("sqlb: construct is not supported by this dialect")

ErrUnsupportedDialect is returned by Build when a construct was rendered through a dialect that does not support it. The construct and dialect names are joined to it. Failing at Build rather than at the database is the rule for every engine-specific construct.

Functions

func DialectName

func DialectName(d Dialect) string

DialectName reports the engine name of d, or DialectUnknown.

func Quote

func Quote(s string, q byte) string

Quote wraps s in q and doubles any embedded q. Exported so an engine package can implement QuoteIdent with a different quote character.

Types

type Args

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

Args collects bound values during one render and hands out the placeholder for each. It also carries the first render error, so render functions keep a single string return value and Build still fails closed.

func NewArgs

func NewArgs(d Dialect) *Args

NewArgs starts an empty argument list for d.

func NewInlineArgs

func NewInlineArgs(d Dialect) *Args

NewInlineArgs returns Args whose Next writes every value as a literal instead of binding it, for a statement that takes no parameters at all.

DDL is that statement. No engine accepts a placeholder in a DEFAULT, a CHECK, a partial index's WHERE or a view's body: PREPARE rejects every DDL statement at the Postgres server, and SQLite rejects ? in CREATE TABLE. Those positions render through the same expression constructs a query uses, so rather than a second renderer for each construct, the Args decides: bound here, written inline there. Values stays empty, which is what makes a DDL statement's Args a mistake to pass to the driver.

A value the dialect cannot write faithfully fails Build with the dialect's error, and a dialect without LiteralQuoting fails with ErrNoLiteralQuoting; neither writes a best guess.

func (*Args) Bare

func (a *Args) Bare(fn func() string) string

Bare renders fn with every column written by its bare name, and restores the previous mode afterwards, so a CHECK inside CREATE TABLE and a view body in the same statement each get what they need. It is a separate switch from the inline mode for that reason: a view body needs literals and qualified names, a CHECK needs literals and bare ones.

func (*Args) ByTableName

func (a *Args) ByTableName() bool

ByTableName reports whether a column should be qualified by its table's name rather than its alias.

A statement that takes no alias but exposes a second relation of the same columns needs it: MySQL's INSERT ... AS new ON DUPLICATE KEY UPDATE takes no table alias, so "i"."hits" is an unknown column, and exposes new.hits, so a bare "hits" is ambiguous. Only "c_items"."hits" names the existing row.

func (*Args) Dialect

func (a *Args) Dialect() Dialect

Dialect returns the dialect this render is for.

func (*Args) Err

func (a *Args) Err() error

Err returns the first error recorded during the render, or nil.

func (*Args) Fail

func (a *Args) Fail(err error)

Fail records the first render error. Later errors are dropped because the statement is already unusable. Build returns the recorded error.

func (*Args) Literal

func (a *Args) Literal(v any) string

Literal writes v as a literal through the dialect's LiteralQuoting, whatever mode the Args is in, and records a failure rather than writing a guess.

It is for the positions that never take a parameter even in a query, a jsonpath or a text-search configuration, and it is the one place a literal is written: Next in inline mode calls it, and so does EngineQuoteLit.

func (*Args) Next

func (a *Args) Next(v any) string

Next appends v and returns the placeholder for its position.

func (*Args) TableNamed

func (a *Args) TableNamed(fn func() string) string

TableNamed renders fn with every column qualified by its table's name, and restores the previous mode afterwards, as Bare does.

func (*Args) Unqualified

func (a *Args) Unqualified() bool

Unqualified reports whether a column should render by its bare name.

Inside a statement's own definition a column cannot be qualified: a CHECK in CREATE TABLE users writes "status", not "u"."status", because the alias does not exist there. A column construct asks this and drops its qualifier; every other construct ignores it.

func (*Args) Unsupported

func (a *Args) Unsupported(construct string)

Unsupported records that construct cannot render through this dialect. The first error wins; later ones are dropped because the statement is already unusable. Build returns the recorded error.

func (*Args) Values

func (a *Args) Values() []any

Values returns the bound values in placeholder order.

type CaseInsensitiveLike

type CaseInsensitiveLike interface {
	SupportsILike() bool
}

CaseInsensitiveLike is satisfied by a dialect with an ILIKE operator, a LIKE that ignores case.

Postgres is the one engine that has it, and it is the one engine that needs it: measured on Postgres 17, 'Ada' LIKE 'ada%' is false, while MySQL under its default collation and SQLite for ASCII both answer true. A predicate that must match regardless of case asks for this and writes ILIKE, which keeps the column bare; without it the predicate writes lower(column) LIKE lower(pattern), which every engine accepts. Both spellings answer the same question, so the capability chooses a spelling and never a meaning.

It is a capability rather than a check of the engine's name so that a dialect from another repository, CockroachDB say, gets ILIKE by implementing one method, and one that declines gets the portable form rather than an error. See expr.ContainsFold.

type CombinedAlter

type CombinedAlter interface {
	SupportsCombinedAlter() bool
}

CombinedAlter is satisfied by dialects that accept several actions in one ALTER TABLE, separated by commas. Postgres does, and applies them under one lock and at most one table rewrite; SQLite accepts exactly one action per statement. A DDL builder writes one statement per action for a dialect without it, so the author's code is the same either way.

type Dialect

type Dialect interface {
	Placeholder(n int) string
	QuoteIdent(ident string) string
}

Dialect is the two things SQL engines disagree on in standard SQL: how a bound parameter is written and how an identifier is quoted.

It is deliberately two methods. A wide dialect interface is where query builders go to die. Anything else an engine has or lacks is expressed as an optional interface (see Named, Returning) so a dialect never has to lie about a capability it does not have.

type InsertAliasing

type InsertAliasing interface {
	SupportsInsertAlias() bool
}

InsertAliasing is satisfied by dialects that accept "INSERT INTO t AS alias", which Postgres does and MySQL does not. With it, RETURNING and ON CONFLICT ... DO UPDATE may use alias-qualified columns; without it the INSERT renders the bare table and RETURNING renders bare column names.

type LikeEscaping

type LikeEscaping interface {
	SupportsLikeEscape() bool
}

LikeEscaping is satisfied by dialects that accept an ESCAPE clause on LIKE.

It exists because escaping a LIKE pattern is not portable in the way it looks. Postgres and MySQL treat a backslash in a pattern as an escape character by default; SQLite has no default escape character at all, so a pattern escaped with backslashes matches nothing unless ESCAPE is written out. That difference is invisible: the statement runs and returns no rows.

A predicate that must escape a user's text therefore asks the dialect first and records Args.Unsupported when the answer is no, rather than rendering something that silently matches the wrong rows. See expr.Contains.

type LiteralQuoting

type LiteralQuoting interface {
	QuoteLiteral(v any) (string, error)
}

LiteralQuoting is satisfied by a dialect that can write a value into the statement text as a literal the engine reads back as exactly that value.

Binding is always the better answer and everything that can be bound is. This is for the positions that take no parameter: a jsonpath or a text-search configuration a function wants as a constant, and every value in DDL, which no engine will parameterise at all. A literal is the one place a value becomes statement text, so a wrong quoting rule here is an injection or, subtler and worse, a value the engine reads differently from what was written. Postgres reads a backslash in a plain string literal as an escape when standard_conforming_strings is off, so the same text stores different bytes depending on how the server is configured; a correct Postgres implementation writes such a string in the E'...' form, which reads the same either way.

The argument is any for the reason Args.Next takes any: this is Next's inline counterpart, and a value arrives from a typed Expr[T] whose T is open. The set a dialect renders is closed. A value outside it, or one the engine could not store faithfully (a string holding NUL, a NaN), is ErrNotInlinable; it is never written by a best guess.

A dialect declines the capability by not implementing the interface, and a literal requested of such a dialect fails with ErrNoLiteralQuoting.

type Named

type Named interface {
	Name() string
}

Named is satisfied by dialects that can name their engine. The name appears in ErrUnsupportedDialect messages and is what engine packages compare against. Dialects that do not implement it are reported as DialectUnknown.

type NumericParameters

type NumericParameters interface {
	NumericParam(placeholder string, v any) string
}

NumericParameters is satisfied by a dialect that widens an untyped integer parameter when it meets a DECIMAL, and says how to give it a type. MySQL is the case: sum() of an integer column is DECIMAL there, and COALESCE(sum(x), ?) or sum(x) + ? in a prepared statement comes back as DECIMAL(65,30), so the value 40 arrives as "40.000000000000000000000000000000" and cannot be scanned into the int64 an Expr[int64] promises. Measured on MySQL 8.4.11 on 2026-09-28 by sqlbtest's crud_transcript case; CASE and NULLIF with the same parameter do not widen, and neither does a float parameter.

A construct that unifies a bare parameter with another operand's type, COALESCE and the arithmetic operators, asks for this and writes NumericParam(placeholder, value) instead of the placeholder alone. v is the value just bound, typed any for the reason Args.Next takes any. An implementation returns the placeholder unchanged for a value it has no reason to type. Postgres and SQLite decline it: neither widens.

type Returning

type Returning interface {
	SupportsReturning() bool
}

Returning is satisfied by dialects that support INSERT ... RETURNING. A write builder type-asserts for it and uses the documented two-statement fallback when it is absent.

type SQLer

type SQLer interface {
	SQL(a *Args) string
}

SQLer is anything that can render itself into SQL text using the statement's Args. It is the one interface every construct in every layer above satisfies, which is what lets a raw fragment and a typed expression occupy the same position.

func Raw

func Raw(render func(a *Args) string) SQLer

Raw makes a SQLer from a render function. It is the way into the builder for a fragment this module has no type for, and the reason going raw costs type checking rather than safety: the function still receives Args, so values are still bound and placeholders still numbered by the dialect.

type UnrestrictedAddColumn

type UnrestrictedAddColumn interface {
	SupportsUnrestrictedAddColumn() bool
}

UnrestrictedAddColumn is satisfied by dialects whose ALTER TABLE ... ADD COLUMN accepts any column CREATE TABLE would. Postgres does. SQLite does not: it refuses a NOT NULL column without a default, a non-constant default, and a key constraint on the added column, each at run time with an error about syntax. A DDL builder refuses those at Build for a dialect without this, so the author learns the rule rather than the error.

type UntypedParameters

type UntypedParameters interface {
	TextParam(placeholder string) string
}

UntypedParameters is satisfied by a dialect that cannot infer a type for a bound parameter passed to a function taking any type, and says how to give it one. Postgres is the case: concat($1) fails with "could not determine data type of parameter", measured on Postgres 17 by the conformance suite, while MySQL and SQLite type the parameter themselves and so decline this. A construct that passes a bare parameter to such a function asks for it and writes TextParam(placeholder) instead of the placeholder alone.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL