Help us shape the future of Go! Take the 2026 Go Developer Survey

gohan

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 27, 2026 License: Apache-2.0 Imports: 10 Imported by: 4

README

gohan

gohan is a SQL query builder where values are always bound and identifiers are always quoted and escaped by construction, so a caller cannot reintroduce SQL injection through the normal API.

Three escape hatches exist for trusted input only, and must never be built from request data: Raw's SQL text, Fn's function name, and Cast's type string.

Install

go get github.com/oddbit-project/gohan

Overview

  • Select, Insert, Update, Delete builders, each immutable — every method returns a new value and never mutates the receiver.
  • Three built-in dialects: gohan.Postgres(), gohan.SQLite(), gohan.ClickHouse() (plus gohan.Generic(), ANSI SQL — never register it for MySQL, where a double-quoted string is a literal, not an identifier).
  • Build(dialect) (string, []any, error) renders a statement; nothing runs a query — gohan has no I/O.
  • The field subpackage (github.com/oddbit-project/gohan/field) extracts struct metadata for Insert(...).Rows(...) and Update(...).SetRecord(...); see "Struct tags" below.

String-position rule

Whether a Go string is treated as a column name or as a bound value depends on where it appears:

  • Column position: the argument to Count, Sum, Avg, Min, Max, select-list columns, GROUP BY, ORDER BY, and INSERT/UPDATE column names — a string there is a column name.
  • Value position: comparison right-hand sides, Fn arguments, Raw arguments, Cast's value, Case's WHEN/THEN/ELSE, and Val — a string there is a bound value. Pass Col("x") when a column is meant.

SELECT, WHERE, ORDER BY, LIMIT

sql, args, err := gohan.Select("id", "name").
    From("users").
    Where(gohan.Col("active").Eq(true)).
    OrderBy(gohan.Col("name").Asc()).
    Limit(10).
    Build(gohan.Postgres())
// SELECT "id", "name" FROM "users" WHERE "active" = $1 ORDER BY "name" ASC LIMIT 10 [true]

(example_test.go's ExampleSelect.)

INSERT and upsert

sql, args, err := gohan.Insert("users").
    Columns("id", "name", "email").
    Values(1, "alice", "alice@example.com").
    OnConflict("id").
    DoUpdateExcluded("name", "email").
    Build(gohan.Postgres())
// INSERT INTO "users" ("id", "name", "email") VALUES ($1, $2, $3)
// ON CONFLICT ("id") DO UPDATE SET "name" = excluded."name", "email" = excluded."email"

(ExampleInsert_upsert.) Rows(records...) builds an INSERT from structs instead of Values/Columns; gohan.InsertColumns(rec) and gohan.RecordColumns(t) report the column list a record would use without building a statement. OnConflict(cols...).DoNothing() / .DoUpdate(map[string]any{...}) are the other two conflict actions.

UPDATE

sql, args, err := gohan.Update("users").
    Set("name", "bob").
    Where(gohan.Col("id").Eq(1)).
    Build(gohan.Postgres())
// UPDATE "users" SET "name" = $1 WHERE "id" = $2 [bob 1]

(ExampleUpdate.) SetRecord(rec, opts...) sets one col = v per updatable field of a struct, with RecordOptions (SkipZeroValues, IncludeFields, ExcludeFields, WithAutoFields) to control which fields are written.

DELETE and UPDATE require a WHERE

Delete and Update refuse to build with no WHERE clause, or one that is trivially true (e.g. And() with no arguments, or an empty NotIn) — there is no accidental "delete every row". Both fail at Build with gohan.ErrNoWhere:

sql, args, err := gohan.Delete("users").Build(gohan.Postgres())
// [] gohan: statement requires a WHERE clause; call All() to affect every row

(ExampleDelete_requiresWhere.) A caller who means to affect every row calls .All(): gohan.Delete("users").All(). On ClickHouse specifically, Delete(t).All() with no other condition renders ... WHERE 1 (ClickHouse's lightweight-delete syntax requires a WHERE clause even to select every row); on PostgreSQL/SQLite the WHERE clause is omitted entirely.

The trivially-true/false check sees through Not/Or and Val/subqueries, not just a bare And()/NotIn(): Not(gohan.Or()) (the negation of an always-false OR) is recognized as always-true and refused the same way. This check is per-condition, not per-statement: a WHERE that combines a real, non-trivial condition with a trivially-true one — e.g. Where(gohan.Col("tenant").Eq(id), gohan.Col("status").NotIn()) ("tenant, and status not in nothing") — is not refused, because the overall AND is not trivially true; it deletes/updates every row of that tenant. NotIn() with no arguments is only ever a bug in the caller's own condition-building code, not a caller expressing "no rows" — write the condition you mean instead of relying on the guard to catch this shape.

Raw: the escape hatch

Raw(sql, args...) renders sql text verbatim, substituting each ? marker with the corresponding argument (bound, or rendered inline if it is itself an Expr); ?? writes a literal ? without consuming an argument — needed because PostgreSQL itself uses a bare ? as an operator (e.g. jsonb's key-exists operator, data ? 'key'), which would otherwise collide with Raw's own placeholder syntax. Raw's SQL text is trusted input and must never be built from request data.

sql, args, err := gohan.Select().
    From("docs").
    Where(gohan.Raw("data ?? ?", "owner")).
    Build(gohan.Postgres())
// SELECT * FROM "docs" WHERE (data ? $1) [owner]

(ExampleRaw.) Note the parentheses: a Raw expression is parenthesized when it is rendered as an operand of And/Or/Not/a comparison (WHERE (data ? $1) above) or as a select-list column (SELECT (k + 1) ...), so it composes safely next to other expressions there. It renders bare — no parentheses — as an UPDATE/DO UPDATE SET value (SET "x" = y + 1, not SET "x" = (y + 1)), including through Val(Raw(...)), which unwraps to the same Raw value. ?? is rejected outright on ClickHouse (ErrRawPlaceholder) — there is no literal ? in ClickHouse Raw text. A $ followed by a digit is also rejected everywhere (it would collide with PostgreSQL's own $n placeholder syntax).

Int for integer constants

Int(n int64) renders n as an inline integer literal (negative values parenthesized), for places where a bound constant would be typed wrong. The canonical case is an integer constant inside a CASE fed to SUM on PostgreSQL: a bound int argument is sent as text, and SUM(CASE ... THEN $1 ...) fails with function sum(text) does not exist. Int avoids the bind entirely:

sql, args, err := gohan.Select(
    gohan.Sum(gohan.Case().
        When(gohan.Col("status").Eq("done"), gohan.Int(1)).
        Else(gohan.Int(0))).As("done_count"),
).From("tasks").Build(gohan.Postgres())
// SELECT SUM(CASE WHEN "status" = $1 THEN 1 ELSE 0 END) AS "done_count" FROM "tasks" [done]

(ExampleCase.) Sub(q) renders a scalar subquery, (<select>), sharing the outer statement's placeholder counter.

UNION

q := gohan.Select("id").From("t1").Union(gohan.Select("id").From("t2"))

sqlPg, _, _ := q.Build(gohan.Postgres())
// SELECT "id" FROM "t1" UNION SELECT "id" FROM "t2"

sqlCh, _, _ := q.Build(gohan.ClickHouse())
// SELECT "id" FROM "t1" UNION DISTINCT SELECT "id" FROM "t2"

(ExampleSelectBuilder_Union.) UnionAll is always UNION ALL. A UNION member cannot have its own ORDER BY, LIMIT, OFFSET, WITH, SETTINGS or UNION (ErrCompoundPart). On ClickHouse, a UNION with an outer ORDER BY/LIMIT/OFFSET is wrapped as SELECT * FROM (<union>) ORDER BY ... LIMIT ... — ClickHouse's own UNION grammar does not accept a trailing ORDER BY/LIMIT the way PostgreSQL/SQLite do.

SelectBuilder.IsCompound() reports whether a builder has any UNION members. Where/Having/ Prewhere called on a compound builder apply to the first member's core only — not the whole result set — while OrderBy/Limit/Offset apply to the compound as a whole; a caller that needs a filter applied across the whole result set (e.g. a repository layer that rejects a compound base query outright) must wrap the compound in a subquery first: gohan.From(q.As("u")).

ClickHouse-specific clauses

FeatureClickHouse gates Final(), Sample(ratio)/SampleRows(n), ArrayJoin/ LeftArrayJoin, Prewhere, and Settings(map[string]any) — each fails with ErrUnsupported on a dialect without the feature. ClickHouse identifiers additionally reject ?, @, {, } and $ followed by a digit (ErrInvalidIdentifier) — these would collide with ClickHouse's own parameterized-query and settings placeholder syntax. Values are checked recursively: a map, struct, or a statement builder passed where a value is expected fails with ErrUnsafeValue / ErrInvalidColumn rather than being sent to the driver, since clickhouse-go does not bind these safely (verified against clickhouse-go v2.40.3). This recursion also rejects a database/sql/driver.Valuer nested inside a slice, pointer or interface argument (ErrUnsafeValue) unless it implements fmt.Stringer — clickhouse-go only calls Value() on a Valuer that is itself the top-level bound argument, so a nested one would otherwise reach the driver unconverted; pass it unwrapped as its own top-level value, or via In(...), which binds each element at top level. A top-level nil pointer whose type implements driver.Valuer is bound as NULL instead of being passed to the driver, which would otherwise panic calling a value-receiver Value() method on a nil pointer. ClickHouse's LEFT JOIN fills non-matching columns with each column's type default (0, '', etc.), not NULL, unless the join_use_nulls = 1 setting is used — a gohan-built LEFT JOIN sends valid SQL either way, but code that checks a joined column for NULL needs that setting set (via .Settings(...)) to see one.

SQLite identifiers

SQLite identifiers are quoted with backticks, not double quotes: SQLite treats an unrecognized double-quoted identifier as a string literal instead of raising an error, which is exactly the kind of silent misbehavior gohan is built to avoid. gohan.SQLite()'s upsert-from-select (InsertBuilder.FromSelect combined with OnConflict) also adds a WHERE true to the last select core when it has none — SQLite's own upsert grammar is otherwise ambiguous between the SELECT's WHERE and the ON CONFLICT clause.

LIKE case sensitivity

On SQLite, LIKE is ASCII case-insensitive; on PostgreSQL, ClickHouse and Generic, LIKE is case-sensitive (use ILIKE there, gated by FeatureILike). Contains, HasPrefix and HasSuffix build a LIKE pattern with %/_ in the input escaped, so untrusted search text cannot inject its own wildcards.

Record shapes

gohan.RecordColumns(t) / gohan.InsertColumns(rec) (and every place gohan reads a struct — Insert(...).Rows(...), Update(...).SetRecord(...)) reject field shapes the field package cannot map unambiguously, rather than silently mapping the wrong column:

  • an embedded pointer struct, an unexported or explicitly tagged embedded struct, or an ambiguous promoted field name — ErrRecordShape;
  • the same column set more than once — ErrDuplicateColumn.

Struct tags

The field subpackage (github.com/oddbit-project/gohan/field) extracts Metadata from a struct's exported fields via GetStructMeta, honouring these struct tags:

  • db:"name" / ch:"name" — the column name (checked in that order; ch is a ClickHouse-style alias for db). A value of - (db:"-") excludes the field entirely. Additional comma-separated values after the name are kept as DbOptions, except auto, which sets Auto.
  • auto:"true" — marks the field as automatically generated (e.g. a serial primary key or a DB-side default); Auto fields are skipped by Insert(...).Rows(...) and, unless WithAutoFields() is passed, by Update(...).SetRecord(...).
  • goqu:"skipinsert" / goqu:"skipupdate" — also set Auto (kept for compatibility with the goqu tag vocabulary). goqu:"omitnil" sets OmitNil (a nil pointer field is skipped); goqu:"omitempty" sets OmitEmpty (a zero-value field is skipped).
  • grid:"sort" / grid:"filter" / grid:"search" — set Sortable / Filterable / Searchable, for callers that expose a struct through a sortable/filterable listing. grid:"auto" is an alias for auto:"true". Any other value in a grid tag is kept as a DbOptions entry.
  • json:"name", xml:"name", alias:"name" — the field's Alias (checked in that order; alias wins over json/xml). Defaults to the Go field name.
  • Reserved types: a struct-typed field whose type name is registered as "reserved" (time.Time by default) is treated as a leaf value, not recursed into as an embedded struct. field.AddReservedType(name) registers another type (e.g. a custom time or UUID wrapper); field.GetReservedTypes() / field.IsReservedType(name) read the registry back.
  • Anonymous (embedded) struct fields are flattened into their parent's field list, unless the embedded field is a pointer, is unexported, or itself carries a db/ch tag — those shapes are rejected by the statement builders (ErrRecordShape) rather than mapped ambiguously.

Dialect support

Feature PostgreSQL SQLite ClickHouse
Placeholders $n ? ?
Identifier quoting "..." `...` "..." (backslash-escaped, restricted charset)
RETURNING yes yes no
ON CONFLICT upsert yes yes no
UPDATE statement yes yes no
ILIKE yes no (use case-insensitive LIKE) yes
UNION keyword UNION UNION UNION DISTINCT
FINAL/SAMPLE/ARRAY JOIN/PREWHERE/SETTINGS no no yes
Bound-argument limit 65535 32766 none

Limitations

  • Raw's SQL text, Fn's function name and Cast's type string are trusted input: never build them from request data — nothing downstream escapes them.
  • ClickHouse has no UPDATE, RETURNING, ON CONFLICT upsert, or transactions.
  • ClickHouse's lightweight DELETE does not work on Distributed tables or tables with projections — a gohan-built DELETE still sends valid SQL, but whether ClickHouse accepts it depends on the table engine.
  • A bound value alone in a PostgreSQL select list may need Cast(...) to give it a concrete type (PostgreSQL infers text for an untyped bound parameter in some positions).

Security

Please report vulnerabilities privately — see SECURITY.md. Do not open public issues for security problems.

Every push, pull request and weekly schedule runs Trivy over the repository (vulnerabilities, secrets, misconfigurations; results in GitHub code scanning) and over a CycloneDX SBOM generated with cyclonedx-gomod; CRITICAL/HIGH findings fail the build. Tagged releases carry sbom.json. Gohan has no runtime dependencies, so its SBOM lists no components — test-only dependencies are still covered by the repository scan. Locally: make sbom, make scan.

License

Apache-2.0. See LICENSE.

Documentation

Overview

Package gohan is a SQL query builder where values are always bound and identifiers are always quoted and escaped by construction, so a caller cannot reintroduce SQL injection through the normal API.

Three escape hatches exist for trusted input only, and must never be built from request data: Raw's sql text, Fn's function name, and Cast's type string.

String position rule: whether a string is treated as a column name or as a bound value depends on where it appears.

  • Column position: the argument to Count, Sum, Avg, Min, Max, select lists, GROUP BY, ORDER BY, and INSERT/UPDATE column names is a column name if it is a string.
  • Value position: comparison right-hand sides, Fn arguments, Raw arguments, Cast's value, Case's THEN/ELSE, and Val treat a string as a bound value. Pass Col("x") when a column is meant.

Generic assumes ANSI double-quoted identifiers and must not be registered for MySQL, where a double-quoted string is a string literal, not an identifier.

On SQLite, LIKE is ASCII case-insensitive; on PostgreSQL, ClickHouse and Generic, LIKE is case-sensitive.

Index

Examples

Constants

View Source
const (
	ErrUnknownDialect    = Error("gohan: unknown or zero dialect")
	ErrInvalidIdentifier = Error("gohan: invalid identifier")
	ErrUnsupported       = Error("gohan: not supported by dialect")
	ErrRawArgs           = Error("gohan: raw placeholder count does not match arguments")
	ErrRawPlaceholder    = Error("gohan: raw sql contains a forbidden placeholder sequence")
	ErrInvalidFunction   = Error("gohan: invalid function name")
	ErrInvalidType       = Error("gohan: invalid cast type")
	ErrEmptyMatch        = Error("gohan: Match requires at least one field")
	ErrEmptyCase         = Error("gohan: CASE requires at least one WHEN")
	ErrUnsafeValue       = Error("gohan: value type cannot be bound safely for this dialect")
	ErrTooManyArgs       = Error("gohan: statement exceeds the dialect's bound-argument limit")
	ErrNilExpr           = Error("gohan: nil or zero expression")
	ErrNoWhere           = Error("gohan: statement requires a WHERE clause; call All() to affect every row")
	ErrNoTable           = Error("gohan: statement has no table")
	ErrNeedAlias         = Error("gohan: subquery in FROM requires an alias")
	ErrInvalidColumn     = Error("gohan: column must be a string or an expression")
	ErrInvalidLimit      = Error("gohan: LIMIT/OFFSET out of range for dialect")

	ErrNoColumns        = Error("gohan: statement has no columns to write")
	ErrValueCount       = Error("gohan: value count does not match column count")
	ErrInsertMixed      = Error("gohan: Rows, Values, SetMap and FromSelect cannot be combined")
	ErrInvalidRecord    = Error("gohan: record must be a non-nil struct or pointer to struct")
	ErrRecordType       = Error("gohan: all records must have the same type")
	ErrRecordShape      = Error("gohan: record type has a field shape gohan cannot map safely")
	ErrInconsistentOmit = Error("gohan: records disagree on omitted fields")
	ErrUnknownField     = Error("gohan: field not found in record")
	ErrDuplicateColumn  = Error("gohan: column set more than once")
	ErrConflictTarget   = Error("gohan: DO UPDATE requires conflict columns")

	ErrCompoundPart   = Error("gohan: a UNION member cannot have ORDER BY, LIMIT, OFFSET, WITH, SETTINGS or its own UNION")
	ErrInvalidSample  = Error("gohan: SAMPLE ratio must be in (0, 1]")
	ErrInvalidSetting = Error("gohan: invalid SETTINGS name")
	ErrEmptyClause    = Error("gohan: clause requires at least one column")
)

Variables

This section is empty.

Functions

func InsertColumns

func InsertColumns(rec any) ([]string, error)

InsertColumns returns the columns an INSERT of rec would write: every mapped column except Auto fields, a nil pointer with OmitNil, and a zero value with OmitEmpty.

func RecordColumns

func RecordColumns(t reflect.Type) ([]string, error)

RecordColumns returns every column t maps (Auto fields included, in field.GetStructMeta order), after the same shape and duplicate checks as recordValues. t may be a struct type or a pointer to one.

func Register

func Register(driverName string, d Dialect)

Register associates driverName with d, overriding any previous registration.

Types

type CaseBuilder

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

CaseBuilder builds a CASE WHEN ... THEN ... [ELSE ...] END expression.

func Case

func Case() CaseBuilder

Case starts a CASE expression.

Example

ExampleCase shows Int for an integer constant inside a CASE expression: on PostgreSQL, a bound constant here would be typed as text and fail SUM's type check ("function sum(text) does not exist"), so integer constants use Int, not a bound value.

package main

import (
	"fmt"

	"github.com/oddbit-project/gohan"
)

func main() {
	sql, args, err := gohan.Select(
		gohan.Sum(gohan.Case().
			When(gohan.Col("status").Eq("done"), gohan.Int(1)).
			Else(gohan.Int(0))).As("done_count"),
	).From("tasks").Build(gohan.Postgres())
	fmt.Println(sql, args, err)
}
Output:
SELECT SUM(CASE WHEN "status" = $1 THEN 1 ELSE 0 END) AS "done_count" FROM "tasks" [done] <nil>

func (CaseBuilder) Else

func (c CaseBuilder) Else(v any) Value

Else adds an ELSE v clause and finishes the CASE expression.

func (CaseBuilder) End

func (c CaseBuilder) End() Value

End finishes the CASE expression without an ELSE clause. No When calls fails with ErrEmptyCase at render.

func (CaseBuilder) When

func (c CaseBuilder) When(cond Expr, then any) CaseBuilder

When adds a WHEN cond THEN then clause.

type ConflictBuilder

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

ConflictBuilder configures the action of an ON CONFLICT clause.

func (ConflictBuilder) DoNothing

func (c ConflictBuilder) DoNothing() *InsertBuilder

DoNothing renders "ON CONFLICT [(cols)] DO NOTHING".

func (ConflictBuilder) DoUpdate

func (c ConflictBuilder) DoUpdate(set map[string]any) *InsertBuilder

DoUpdate renders "ON CONFLICT (cols) DO UPDATE SET ...", with set's keys sorted. A value is rendered in place if it is an Expr (e.g. Excluded or Raw), otherwise bound.

func (ConflictBuilder) DoUpdateExcluded

func (c ConflictBuilder) DoUpdateExcluded(cols ...string) *InsertBuilder

DoUpdateExcluded renders "ON CONFLICT (cols) DO UPDATE SET c = excluded.c" for each c in cols. Each c must be one of the INSERT's own columns, else Build fails with ErrUnknownField.

type DeleteBuilder

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

DeleteBuilder builds a DELETE statement. Builders are immutable: every method returns a new value and never modifies the receiver.

func Delete

func Delete(table any) *DeleteBuilder

Delete starts a DELETE statement against table: a string or TableRef. A Subquery fails with ErrNoTable at Build.

Example (RequiresWhere)

ExampleDelete_requiresWhere shows that a DELETE with no WHERE clause fails at Build with ErrNoWhere instead of deleting every row. Deleting every row on purpose uses All().

package main

import (
	"fmt"

	"github.com/oddbit-project/gohan"
)

func main() {
	sql, args, err := gohan.Delete("users").Build(gohan.Postgres())
	fmt.Println(sql, args, err)
}
Output:
[] gohan: statement requires a WHERE clause; call All() to affect every row

func (*DeleteBuilder) All

func (d *DeleteBuilder) All() *DeleteBuilder

All allows the DELETE to build with no WHERE clause, or a trivially true one, affecting every row.

func (*DeleteBuilder) Build

func (d *DeleteBuilder) Build(dl Dialect) (string, []any, error)

Build renders the DELETE statement under dialect dl using a fresh writer.

func (*DeleteBuilder) Returning

func (d *DeleteBuilder) Returning(cols ...any) *DeleteBuilder

Returning sets the RETURNING column list. Zero cols means no RETURNING clause.

func (*DeleteBuilder) Where

func (d *DeleteBuilder) Where(conds ...Expr) *DeleteBuilder

Where appends conds, ANDed with any existing conditions.

type Dialect

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

Dialect describes the SQL identifier quoting, placeholder style and feature set of a target database.

func ClickHouse

func ClickHouse() Dialect

ClickHouse returns the ClickHouse dialect: `?` placeholders, double-quoted identifiers with backslash doubling, ILIKE/ClickHouse clause support, and no bound-argument limit.

func DialectFor

func DialectFor(driverName string) (Dialect, error)

DialectFor returns the Dialect registered for driverName, or ErrUnknownDialect if none is registered.

func Generic

func Generic() Dialect

Generic returns the ANSI SQL dialect: `?` placeholders, ANSI double-quoted identifiers, UPDATE support, and a 999 bound-argument limit. It must not be registered for MySQL, where double-quoted text is a string literal, not an identifier.

func Postgres

func Postgres() Dialect

Postgres returns the PostgreSQL dialect: numbered ($n) placeholders, double-quoted identifiers, RETURNING/upsert/ILIKE/UPDATE support, and a 65535 bound-argument limit.

func SQLite

func SQLite() Dialect

SQLite returns the SQLite dialect: `?` placeholders, backtick-quoted identifiers, RETURNING/upsert/UPDATE support, and a 32766 bound-argument limit.

func (Dialect) Has

func (d Dialect) Has(f Feature) bool

Has reports whether the dialect supports the given feature.

func (Dialect) MaxArgs

func (d Dialect) MaxArgs() int

MaxArgs returns the dialect's bound-argument limit, or 0 for unlimited.

func (Dialect) Name

func (d Dialect) Name() string

Name returns the dialect's name, or "" for the zero Dialect.

func (Dialect) QuoteIdent

func (d Dialect) QuoteIdent(name string) (string, error)

QuoteIdent quotes and escapes name per the dialect's identifier rules. It is for adapters that need to name an identifier outside a statement.

type Error

type Error string

func (Error) Error

func (e Error) Error() string

type Expr

type Expr interface {
	// contains filtered or unexported methods
}

Expr is any renderable SQL fragment. It is sealed: only this package can implement it.

func Star

func Star() Expr

Star renders the unquoted `*`.

type Feature

type Feature uint

Feature is a bitmask of optional SQL capabilities a Dialect supports.

const (
	FeatureReturning  Feature = 1 << iota // RETURNING on INSERT/UPDATE/DELETE
	FeatureUpsert                         // INSERT ... ON CONFLICT
	FeatureILike                          // ILIKE operator
	FeatureUpdate                         // UPDATE statement
	FeatureClickHouse                     // FINAL, PREWHERE, SAMPLE, ARRAY JOIN, SETTINGS
)

type InsertBuilder

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

InsertBuilder builds an INSERT statement. Builders are immutable: every method returns a new value and never modifies the receiver.

func Insert

func Insert(table any) *InsertBuilder

Insert starts an INSERT statement against table: a string or a TableRef without an alias.

Example (Upsert)

ExampleInsert_upsert inserts a row and, on a conflicting primary key, updates the other columns from the values that were about to be inserted (DoUpdateExcluded).

package main

import (
	"fmt"

	"github.com/oddbit-project/gohan"
)

func main() {
	sql, args, err := gohan.Insert("users").
		Columns("id", "name", "email").
		Values(1, "alice", "alice@example.com").
		OnConflict("id").
		DoUpdateExcluded("name", "email").
		Build(gohan.Postgres())
	fmt.Println(sql, args, err)
}
Output:
INSERT INTO "users" ("id", "name", "email") VALUES ($1, $2, $3) ON CONFLICT ("id") DO UPDATE SET "name" = excluded."name", "email" = excluded."email" [1 alice alice@example.com] <nil>

func (*InsertBuilder) Build

func (b *InsertBuilder) Build(d Dialect) (string, []any, error)

Build renders the INSERT statement under dialect d using a fresh writer.

func (*InsertBuilder) Columns

func (b *InsertBuilder) Columns(cols ...string) *InsertBuilder

Columns replaces the column list used with Values or FromSelect.

func (*InsertBuilder) FromSelect

func (b *InsertBuilder) FromSelect(q *SelectBuilder) *InsertBuilder

FromSelect makes the INSERT an "INSERT INTO ... SELECT ...", using the column list set by Columns.

func (*InsertBuilder) OnConflict

func (b *InsertBuilder) OnConflict(cols ...string) ConflictBuilder

OnConflict starts an ON CONFLICT clause targeting cols (which may be empty for DoNothing).

func (*InsertBuilder) Returning

func (b *InsertBuilder) Returning(cols ...any) *InsertBuilder

Returning sets the RETURNING column list.

func (*InsertBuilder) Rows

func (b *InsertBuilder) Rows(records ...any) *InsertBuilder

Rows appends one row per record (a struct or pointer to struct). Values are extracted immediately (a snapshot): mutating a record afterwards does not change the statement. The first record decides the column set; a later record whose omit decision (OmitNil/OmitEmpty) differs for any field fails at Build with ErrInconsistentOmit.

func (*InsertBuilder) SetMap

func (b *InsertBuilder) SetMap(m map[string]any) *InsertBuilder

SetMap sets a single row from m, with columns in sorted key order.

func (*InsertBuilder) Values

func (b *InsertBuilder) Values(vals ...any) *InsertBuilder

Values appends one row of values, in column position.

type Order

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

Order is an ORDER BY item. It implements Expr.

func (Order) NullsFirst

func (o Order) NullsFirst() Order

NullsFirst appends "NULLS FIRST".

func (Order) NullsLast

func (o Order) NullsLast() Order

NullsLast appends "NULLS LAST".

type RecordOption

type RecordOption func(*recordOptions)

RecordOption configures UpdateBuilder.SetRecord.

func ExcludeFields

func ExcludeFields(names ...string) RecordOption

ExcludeFields removes the named fields (struct field name or db name) from SetRecord.

func IncludeFields

func IncludeFields(names ...string) RecordOption

IncludeFields restricts SetRecord to only the named fields (struct field name or db name). When set, ExcludeFields is ignored.

func SkipZeroValues

func SkipZeroValues() RecordOption

SkipZeroValues skips fields holding their type's zero value.

func WithAutoFields

func WithAutoFields() RecordOption

WithAutoFields includes fields marked Auto, which are otherwise always skipped by SetRecord.

type SelectBuilder

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

SelectBuilder builds a SELECT statement. Builders are immutable: every method returns a new value and never modifies the receiver.

func From

func From(table any) *SelectBuilder

From is shorthand for Select().From(table).

func Select

func Select(cols ...any) *SelectBuilder

Select starts a SELECT statement with the given select-list columns. A string is a column name; anything else that is not an Expr fails with ErrInvalidColumn at Build.

Example

ExampleSelect builds a SELECT with a WHERE, ORDER BY and LIMIT clause against the PostgreSQL dialect.

package main

import (
	"fmt"

	"github.com/oddbit-project/gohan"
)

func main() {
	sql, args, err := gohan.Select("id", "name").
		From("users").
		Where(gohan.Col("active").Eq(true)).
		OrderBy(gohan.Col("name").Asc()).
		Limit(10).
		Build(gohan.Postgres())
	fmt.Println(sql, args, err)
}
Output:
SELECT "id", "name" FROM "users" WHERE "active" = $1 ORDER BY "name" ASC LIMIT 10 [true] <nil>

func (*SelectBuilder) AddColumns

func (s *SelectBuilder) AddColumns(cols ...any) *SelectBuilder

AddColumns appends to the select list.

func (*SelectBuilder) ArrayJoin

func (s *SelectBuilder) ArrayJoin(cols ...any) *SelectBuilder

ArrayJoin adds an ARRAY JOIN clause over cols (by position; Col("a"). As("x") allowed). Zero columns fails with ErrEmptyClause; calling both ArrayJoin and LeftArrayJoin on the same builder fails with ErrUnsupported. ErrUnsupported unless the dialect has FeatureClickHouse.

func (*SelectBuilder) As

func (s *SelectBuilder) As(alias string) Subquery

As turns the SELECT into a Subquery usable as a FROM source, aliased by alias (a single-part identifier).

func (*SelectBuilder) Build

func (s *SelectBuilder) Build(d Dialect) (string, []any, error)

Build renders the SELECT statement under dialect d using a fresh writer.

func (*SelectBuilder) Columns

func (s *SelectBuilder) Columns(cols ...any) *SelectBuilder

Columns replaces the select list.

func (*SelectBuilder) CrossJoin

func (s *SelectBuilder) CrossJoin(t any) *SelectBuilder

CrossJoin adds a CROSS JOIN against t.

func (*SelectBuilder) Distinct

func (s *SelectBuilder) Distinct() *SelectBuilder

Distinct adds DISTINCT to the select list.

func (*SelectBuilder) Final

func (s *SelectBuilder) Final() *SelectBuilder

Final adds FINAL to the FROM clause. Fails with ErrUnsupported at Build on a dialect without FeatureClickHouse, and also when the FROM source is a Subquery (ClickHouse or not).

func (*SelectBuilder) From

func (s *SelectBuilder) From(table any) *SelectBuilder

From sets the FROM source: a string ("schema.table" allowed), a TableRef, or a Subquery.

func (*SelectBuilder) FullJoin

func (s *SelectBuilder) FullJoin(t any, on Expr) *SelectBuilder

FullJoin adds a FULL JOIN against t, matched by on.

func (*SelectBuilder) GroupBy

func (s *SelectBuilder) GroupBy(cols ...any) *SelectBuilder

GroupBy appends to the GROUP BY list.

func (*SelectBuilder) Having

func (s *SelectBuilder) Having(conds ...Expr) *SelectBuilder

Having appends conds, ANDed with any existing HAVING conditions.

func (*SelectBuilder) IsCompound

func (s *SelectBuilder) IsCompound() bool

IsCompound reports whether s has any UNION members. Where/Having/ Prewhere called on a compound builder apply to the first member's core only, while ORDER BY/LIMIT/OFFSET apply to the whole compound; callers that need filters applied to the whole result set (e.g. a repository layer) should reject a compound base query.

func (*SelectBuilder) Join

func (s *SelectBuilder) Join(t any, on Expr) *SelectBuilder

Join adds an INNER JOIN against t, matched by on.

func (*SelectBuilder) JoinUsing

func (s *SelectBuilder) JoinUsing(t any, cols ...string) *SelectBuilder

JoinUsing adds an INNER JOIN against t, matched by the named columns. Zero columns fails with ErrEmptyClause at Build.

func (*SelectBuilder) LeftArrayJoin

func (s *SelectBuilder) LeftArrayJoin(cols ...any) *SelectBuilder

LeftArrayJoin adds a LEFT ARRAY JOIN clause over cols. Zero columns fails with ErrEmptyClause; calling both ArrayJoin and LeftArrayJoin on the same builder fails with ErrUnsupported. ErrUnsupported unless the dialect has FeatureClickHouse.

func (*SelectBuilder) LeftJoin

func (s *SelectBuilder) LeftJoin(t any, on Expr) *SelectBuilder

LeftJoin adds a LEFT JOIN against t, matched by on.

func (*SelectBuilder) LeftJoinUsing

func (s *SelectBuilder) LeftJoinUsing(t any, cols ...string) *SelectBuilder

LeftJoinUsing adds a LEFT JOIN against t, matched by the named columns. Zero columns fails with ErrEmptyClause at Build.

func (*SelectBuilder) Limit

func (s *SelectBuilder) Limit(n uint64) *SelectBuilder

Limit sets the LIMIT clause. Limit(0) renders LIMIT 0.

func (*SelectBuilder) Offset

func (s *SelectBuilder) Offset(n uint64) *SelectBuilder

Offset sets the OFFSET clause.

func (*SelectBuilder) OrderBy

func (s *SelectBuilder) OrderBy(o ...any) *SelectBuilder

OrderBy appends to the ORDER BY list. An Order is used as-is; a string or Value is rendered ascending.

func (*SelectBuilder) Prewhere

func (s *SelectBuilder) Prewhere(conds ...Expr) *SelectBuilder

Prewhere appends conds, ANDed with any existing PREWHERE conditions. ErrUnsupported unless the dialect has FeatureClickHouse.

func (*SelectBuilder) RightJoin

func (s *SelectBuilder) RightJoin(t any, on Expr) *SelectBuilder

RightJoin adds a RIGHT JOIN against t, matched by on.

func (*SelectBuilder) Sample

func (s *SelectBuilder) Sample(ratio float64) *SelectBuilder

Sample adds SAMPLE ratio to the FROM clause. ratio must be in (0, 1] and finite, else ErrInvalidSample at Build. Fails with ErrUnsupported at Build on a dialect without FeatureClickHouse, and also when the FROM source is a Subquery (ClickHouse or not).

func (*SelectBuilder) SampleRows

func (s *SelectBuilder) SampleRows(n uint64) *SelectBuilder

SampleRows adds SAMPLE n (an absolute row count) to the FROM clause. n must be >= 1, else ErrInvalidSample at Build. Fails with ErrUnsupported at Build on a dialect without FeatureClickHouse, and also when the FROM source is a Subquery (ClickHouse or not).

func (*SelectBuilder) Settings

func (s *SelectBuilder) Settings(m map[string]any) *SelectBuilder

Settings merges m into the SETTINGS clause: existing keys are overwritten, new keys are added. m is copied into a sorted []kv; mutating m after the call has no effect. Keys must match ^[A-Za-z_][A-Za-z0-9_]*$, else ErrInvalidSetting at Build. ErrUnsupported unless the dialect has FeatureClickHouse.

func (*SelectBuilder) Union

Union adds a UNION member: UNION DISTINCT on ClickHouse, UNION elsewhere. q may not have its own ORDER BY, LIMIT, OFFSET, WITH, SETTINGS or UNION — such a member fails with ErrCompoundPart at Build.

Example

ExampleSelectBuilder_Union shows the UNION keyword difference between PostgreSQL (UNION) and ClickHouse (UNION DISTINCT).

package main

import (
	"fmt"

	"github.com/oddbit-project/gohan"
)

func main() {
	q := gohan.Select("id").From("t1").
		Union(gohan.Select("id").From("t2"))

	sqlPg, _, errPg := q.Build(gohan.Postgres())
	fmt.Println(sqlPg, errPg)

	sqlCh, _, errCh := q.Build(gohan.ClickHouse())
	fmt.Println(sqlCh, errCh)
}
Output:
SELECT "id" FROM "t1" UNION SELECT "id" FROM "t2" <nil>
SELECT "id" FROM "t1" UNION DISTINCT SELECT "id" FROM "t2" <nil>

func (*SelectBuilder) UnionAll

func (s *SelectBuilder) UnionAll(q *SelectBuilder) *SelectBuilder

UnionAll adds a UNION ALL member. q may not have its own ORDER BY, LIMIT, OFFSET, WITH, SETTINGS or UNION — such a member fails with ErrCompoundPart at Build.

func (*SelectBuilder) Where

func (s *SelectBuilder) Where(conds ...Expr) *SelectBuilder

Where appends conds, ANDed with any existing conditions.

func (*SelectBuilder) With

func (s *SelectBuilder) With(name string, q *SelectBuilder) *SelectBuilder

With adds a WITH entry: "name AS (q)". name must be a single-part identifier.

func (*SelectBuilder) WithRecursive

func (s *SelectBuilder) WithRecursive(name string, q *SelectBuilder) *SelectBuilder

WithRecursive adds a WITH RECURSIVE entry: "name AS (q)". name must be a single-part identifier. If any CTE on the builder was added with WithRecursive, the whole WITH clause renders RECURSIVE.

type Statement

type Statement interface {
	Build(d Dialect) (string, []any, error)
}

Statement is anything that renders to a complete SQL string and its bound arguments.

type Subquery

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

Subquery is a SELECT usable as a FROM/JOIN source. It is not an Expr and cannot appear where an expression is expected.

type TableRef

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

TableRef names a table, with an optional alias.

func Table

func Table(name string) TableRef

Table names table for use as a FROM/DELETE target.

func (TableRef) As

func (t TableRef) As(alias string) TableRef

As sets a single-part alias for the table.

func (TableRef) Col

func (t TableRef) Col(name string) Value

Col renders name qualified by the table's alias (if set) or its name.

type UpdateBuilder

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

UpdateBuilder builds an UPDATE statement. Builders are immutable: every method returns a new value and never modifies the receiver.

func Update

func Update(table any) *UpdateBuilder

Update starts an UPDATE statement against table: a string or a TableRef without an alias.

Example

ExampleUpdate builds an UPDATE. A nil or trivially-true WHERE is rejected; see ExampleDelete_requiresWhere.

package main

import (
	"fmt"

	"github.com/oddbit-project/gohan"
)

func main() {
	sql, args, err := gohan.Update("users").
		Set("name", "bob").
		Where(gohan.Col("id").Eq(1)).
		Build(gohan.Postgres())
	fmt.Println(sql, args, err)
}
Output:
UPDATE "users" SET "name" = $1 WHERE "id" = $2 [bob 1] <nil>

func (*UpdateBuilder) All

func (b *UpdateBuilder) All() *UpdateBuilder

All allows the UPDATE to build with no WHERE clause, or a trivially true one, affecting every row.

func (*UpdateBuilder) Build

func (b *UpdateBuilder) Build(d Dialect) (string, []any, error)

Build renders the UPDATE statement under dialect d using a fresh writer.

func (*UpdateBuilder) Returning

func (b *UpdateBuilder) Returning(cols ...any) *UpdateBuilder

Returning sets the RETURNING column list.

func (*UpdateBuilder) Set

func (b *UpdateBuilder) Set(col string, v any) *UpdateBuilder

Set appends "col = v" to the SET list.

func (*UpdateBuilder) SetMap

func (b *UpdateBuilder) SetMap(m map[string]any) *UpdateBuilder

SetMap appends one "col = v" per entry of m, in sorted key order.

func (*UpdateBuilder) SetRecord

func (b *UpdateBuilder) SetRecord(rec any, opts ...RecordOption) *UpdateBuilder

SetRecord appends one "col = v" per updatable field of rec (a struct or pointer to struct), per opts. Values are extracted immediately (a snapshot): mutating rec afterwards does not change the statement. A shape or option error is deferred and reported at Build.

func (*UpdateBuilder) Where

func (b *UpdateBuilder) Where(conds ...Expr) *UpdateBuilder

Where appends conds, ANDed with any existing conditions.

type Value

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

Value is any scalar or boolean expression. All comparison methods live on Value. The zero Value is invalid and fails to render with ErrNilExpr.

func And

func And(exprs ...Expr) Value

And renders exprs joined by AND, in parentheses (unless there is exactly one). And() renders (1=1).

func Avg

func Avg(col any) Value

Avg renders AVG(col). col is a column name if it is a string.

func Cast

func Cast(v any, typ string) Value

Cast renders CAST(v AS typ). typ is trusted input — never build it from request data.

func Col

func Col(name string) Value

Col renders name as a quoted, escaped identifier.

func Count

func Count(col any) Value

Count renders COUNT(col). col is a column name if it is a string.

func CountAll

func CountAll() Value

CountAll renders COUNT(*).

func Excluded

func Excluded(col string) Value

Excluded renders "excluded."col"": the proposed-for-insertion value of col, for use inside DoUpdate. ErrUnsupported on dialects without FeatureUpsert.

func Exists

func Exists(q *SelectBuilder) Value

Exists renders "EXISTS (<select>)". If q's combined WHERE is syntactically never (an empty result set) and q has no UNION members, the returned Value is flagged never for the trivially-true/false guards in delete.go/update.go: this over-approximates (e.g. an aggregate subquery over an empty set still returns one row), which only makes the guard refuse more often, never less.

func Fn

func Fn(name string, args ...any) Value

Fn renders name(args...). name is trusted input — never build it from request data — and must match ^[A-Za-z_][A-Za-z0-9_]*$.

func Int

func Int(n int64) Value

Int renders n as an inline integer literal. Negative values are parenthesized.

func Match

func Match(fields map[string]any) Value

Match renders an AND of equality comparisons, one per field, with keys sorted for deterministic output. Match(nil) or an empty map fails with ErrEmptyMatch.

func Max

func Max(col any) Value

Max renders MAX(col). col is a column name if it is a string.

func Min

func Min(col any) Value

Min renders MIN(col). col is a column name if it is a string.

func Not

func Not(e Expr) Value

Not renders NOT (e). If e is a Value, Not swaps its trivial/never flags: the negation of an always-true condition is always-false and vice versa.

func NotExists

func NotExists(q *SelectBuilder) Value

NotExists renders "NOT EXISTS (<select>)". See Exists for the trivial flag.

func Or

func Or(exprs ...Expr) Value

Or renders exprs joined by OR, in parentheses (unless there is exactly one). Or() renders (1=0).

func Raw

func Raw(sql string, args ...any) Value

Raw renders sql text verbatim, substituting each `?` marker with the corresponding argument (bound, or rendered inline if it is an Expr). `??` writes a literal `?`, except on ClickHouse where it is rejected. sql is trusted input — never build it from request data.

Example

ExampleRaw shows Raw's two placeholder forms with a real PostgreSQL operator that itself uses `?`: jsonb's key-exists operator. `??` writes a literal `?` (the operator) without consuming an argument; `?` binds the argument in position.

package main

import (
	"fmt"

	"github.com/oddbit-project/gohan"
)

func main() {
	sql, args, err := gohan.Select().
		From("docs").
		Where(gohan.Raw("data ?? ?", "owner")).
		Build(gohan.Postgres())
	fmt.Println(sql, args, err)
}
Output:
SELECT * FROM "docs" WHERE (data ? $1) [owner] <nil>

func Sub

func Sub(q *SelectBuilder) Value

Sub renders a scalar subquery: "(<select>)". It shares the outer statement's writer so placeholders keep counting across nesting.

func Sum

func Sum(col any) Value

Sum renders SUM(col). col is a column name if it is a string.

func Val

func Val(v any) Value

Val binds v as an argument. Val(nil) renders the keyword NULL. Val(expr) renders expr in place.

func (Value) As

func (v Value) As(alias string) Expr

As renders "v AS alias". alias must be a single-part identifier.

func (Value) Asc

func (v Value) Asc() Order

Asc renders "v ASC".

func (Value) Between

func (v Value) Between(lo, hi any) Value

Between renders "v BETWEEN lo AND hi".

func (Value) Contains

func (v Value) Contains(s string) Value

Contains renders a LIKE match for "%s%", with LIKE wildcards in s escaped.

func (Value) Desc

func (v Value) Desc() Order

Desc renders "v DESC".

func (Value) Eq

func (v Value) Eq(x any) Value

Eq renders "v = x", or "v IS NULL" when x is nil.

func (Value) Gt

func (v Value) Gt(x any) Value

func (Value) Gte

func (v Value) Gte(x any) Value

func (Value) HasPrefix

func (v Value) HasPrefix(s string) Value

HasPrefix renders a LIKE match for "s%", with LIKE wildcards in s escaped.

func (Value) HasSuffix

func (v Value) HasSuffix(s string) Value

HasSuffix renders a LIKE match for "%s", with LIKE wildcards in s escaped.

func (Value) ILike

func (v Value) ILike(p any) Value

ILike renders "v ILIKE p". ErrUnsupported on dialects without ILIKE.

func (Value) In

func (v Value) In(values ...any) Value

In renders "v IN (values...)", expanding a single slice argument. With no values (after expansion) it renders 1=0. A single *SelectBuilder argument renders "v IN (<select>)".

func (Value) IsNotNull

func (v Value) IsNotNull() Value

IsNotNull renders "v IS NOT NULL".

func (Value) IsNull

func (v Value) IsNull() Value

IsNull renders "v IS NULL".

func (Value) Like

func (v Value) Like(p any) Value

Like renders "v LIKE p".

func (Value) Lt

func (v Value) Lt(x any) Value

func (Value) Lte

func (v Value) Lte(x any) Value

func (Value) Neq

func (v Value) Neq(x any) Value

Neq renders "v <> x", or "v IS NOT NULL" when x is nil.

func (Value) NotBetween

func (v Value) NotBetween(lo, hi any) Value

NotBetween renders "v NOT BETWEEN lo AND hi".

func (Value) NotILike

func (v Value) NotILike(p any) Value

NotILike renders "v NOT ILIKE p". ErrUnsupported on dialects without ILIKE.

func (Value) NotIn

func (v Value) NotIn(values ...any) Value

NotIn renders "v NOT IN (values...)", expanding a single slice argument. With no values (after expansion) it renders 1=1. A single *SelectBuilder argument renders "v NOT IN (<select>)".

func (Value) NotLike

func (v Value) NotLike(p any) Value

NotLike renders "v NOT LIKE p".

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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