types

package
v0.61.0 Latest Latest
Warning

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

Go to latest
Published: Aug 31, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package types contains the core database interface definitions for go-bricks. These interfaces are separate from the main database package to avoid import cycles and to make them easily accessible for mocking and testing.

Package types contains the core database interface definitions for go-bricks.

Package types contains the core database interface definitions for go-bricks.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrEmptyTableName is returned when Table() is called with an empty name.
	ErrEmptyTableName = errors.New("table name cannot be empty")

	// ErrEmptyTableAlias is returned when TableRef.As() is called with an empty alias.
	ErrEmptyTableAlias = errors.New("table alias cannot be empty")

	ErrNilTableRef = errors.New("table reference cannot be nil")

	// ErrOrderingOperandNotComparable is returned when an ordering comparison
	// (<, <=, >, >=) is given an operand that is not a scalar. There is no
	// rendering for one — `col < NULL` is never true whatever the data, and a set
	// has no ordinal meaning — so the door refuses instead of emitting SQL that
	// silently matches nothing.
	//
	// The refused forms are judged AFTER the operand is resolved, so they are
	// wider than their surface spelling: nil; a slice; an array (`[3]int{…}`,
	// which the builder classifies as a list exactly as it does a slice); a typed
	// nil pointer (`(*int)(nil)`, the ordinary spelling of an optional field),
	// which is dereferenced to nil; and a `driver.Valuer` whose Value() resolves
	// to NULL (`sql.NullString{}`). A `[]byte` and a Valuer HOLDING a value stay
	// scalars and are compared normally. A Valuer whose Value() FAILS is a
	// different error: that cause is reported wrapped, not as this sentinel.
	//
	// It lives here rather than in the builder because the builder is an internal
	// package: a sentinel a consumer cannot import is a sentinel a consumer
	// cannot match.
	ErrOrderingOperandNotComparable = errors.New("ordering comparison requires a scalar operand")

	// ErrEmptyExpressionSQL is returned when Expr() is called with empty SQL.
	ErrEmptyExpressionSQL = errors.New("expression SQL cannot be empty")

	// ErrTooManyAliases is returned when Expr() is called with more than 1 alias.
	ErrTooManyAliases = errors.New("expression accepts maximum 1 alias")

	// ErrInvalidAlias is returned when a RawExpression alias is not a bare
	// unquoted identifier. It replaces the former ErrDangerousAlias substring
	// denylist, which accepted everything it did not list.
	ErrInvalidAlias = errors.New("alias must be an unquoted identifier: a letter or underscore followed by letters, digits, underscore, $ or #")

	// ErrAliasInHaving is returned when a RawExpression carrying an alias is passed
	// to Having. HAVING takes a predicate, not a projected column, so an alias has
	// nowhere to render; accepting one silently would drop it.
	ErrAliasInHaving = errors.New("alias is not allowed in a HAVING predicate")

	// ErrNilSubquery is returned when ValidateSubquery() is called with nil subquery.
	ErrNilSubquery = errors.New("subquery cannot be nil")

	// ErrInvalidSubquery is returned when subquery validation fails.
	ErrInvalidSubquery = errors.New("invalid subquery")

	// ErrEmptySubquerySQL is returned when subquery produces empty SQL.
	ErrEmptySubquerySQL = errors.New("subquery produced empty SQL")
)

Sentinel errors for validation failures in type constructors. These can be used with errors.Is() for programmatic error checking.

Functions

func MustValidateSubquery added in v0.19.0

func MustValidateSubquery(subquery SelectQueryBuilder)

MustValidateSubquery is like ValidateSubquery but panics on error. Use this only when errors indicate programming bugs that should crash immediately.

func ValidateSubquery added in v0.14.2

func ValidateSubquery(subquery SelectQueryBuilder) error

ValidateSubquery checks if a subquery is valid for use in filter expressions. Returns an error if subquery is nil or produces invalid SQL.

Returns:

  • ErrNilSubquery if subquery is nil
  • ErrInvalidSubquery if subquery validation fails
  • ErrEmptySubquerySQL if subquery produces empty SQL

This function is called internally by filter implementations to ensure subqueries are constructed correctly before query execution.

Types

type Columns added in v0.15.0

type Columns interface {
	// Col retrieves the vendor-quoted column name for the given struct field name.
	// If aliased via As(), returns qualified column (e.g., "u.id").
	// Panics if the field name is not found (fail-fast for development-time typos).
	//
	// Example (unaliased):
	//   cols.Col("ID")    // Returns: "id" (PostgreSQL) or "id" (Oracle, not a reserved word)
	//   cols.Col("Level") // Returns: "level" (PostgreSQL) or "\"level\"" (Oracle, reserved word — double-quoted lowercase)
	//
	// Example (aliased):
	//   u := cols.As("u")
	//   u.Col("ID")       // Returns: "u.id" (PostgreSQL) or "u.id" (Oracle, not a reserved word)
	Col(fieldName string) string

	// As returns a new Columns instance bound to the specified table alias.
	// The returned instance shares the underlying column metadata (zero-copy).
	// This method is immutable - the original Columns instance remains unchanged.
	//
	// Example:
	//   cols := qb.Columns(&User{})
	//   u := cols.As("u")
	//   p := cols.As("p")
	//   u.Col("ID")  // "u.id"
	//   p.Col("ID")  // "p.id"
	//   cols.Col("ID") // "id" (original unaffected)
	//
	// The alias becomes SQL syntax, so it is validated against the same
	// bare-identifier grammar the table argument applies to the alias half of
	// "users u" (ADR-082). Anything else, an empty alias included, panics with a
	// *InvalidAliasError: As has no error channel, and an alias is a developer
	// constant, so a violation is a programming error surfaced at construction
	// rather than a deferred ToSQL() error.
	As(alias string) Columns

	// Alias returns the current table alias, or empty string if unaliased.
	//
	// Example:
	//   cols.Alias()        // ""
	//   cols.As("u").Alias() // "u"
	Alias() string

	// Cols retrieves vendor-quoted column names for multiple struct field names.
	// If aliased, returns qualified columns.
	// Panics if any field name is not found.
	//
	// Example:
	//   cols.Cols("ID", "Name", "Email") // ["id", "name", "email"]
	//   cols.As("u").Cols("ID", "Name")  // ["u.id", "u.name"]
	Cols(fieldNames ...string) []string

	// All returns vendor-quoted column names for all columns in the struct,
	// in the order they were declared in the struct definition.
	//
	// Example:
	//   cols.All()        // ["id", "name", "email"] (unaliased)
	//   cols.As("u").All() // ["u.id", "u.name", "u.email"] (aliased)
	All() []string

	// FieldMap extracts field values from a struct instance into a map.
	// The map keys are vendor-quoted column names (respecting alias if set).
	// Only fields with `db` tags are included. Zero values are included.
	//
	// Map iteration order is UNSPECIFIED, so building DML straight from this map
	// emits a different column order per process. Sort the keys, or use AllFields,
	// which returns columns and values in declaration order (#1157).
	//
	// Example:
	//   user := User{ID: 123, Name: "Alice", Email: "alice@example.com"}
	//   cols.FieldMap(&user)
	//   // Returns: {"id": 123, "name": "Alice", "email": "alice@example.com"}
	//
	//   cols.As("u").FieldMap(&user)
	//   // Returns: {"u.id": 123, "u.name": "Alice", "u.email": "alice@example.com"}
	//
	// Panics if instance is not a struct or pointer to struct.
	FieldMap(instance any) map[string]any

	// AllFields extracts all field values from a struct instance as separate slices.
	// Returns (columns, values) suitable for bulk INSERT/UPDATE operations.
	// Only fields with `db` tags are included. Zero values are included.
	//
	// Example:
	//   user := User{ID: 123, Name: "Alice", Email: "alice@example.com"}
	//   cols, vals := cols.AllFields(&user)
	//   // cols: ["id", "name", "email"]
	//   // vals: [123, "Alice", "alice@example.com"]
	//
	// Panics if instance is not a struct or pointer to struct.
	AllFields(instance any) ([]string, []any)
}

Columns represents cached metadata for a struct type with `db:"column_name"` tags. It provides methods to retrieve vendor-quoted column names with optional table aliasing.

This interface is implemented by the internal columns package and returned by QueryBuilder.Columns(). The metadata is lazily parsed on first use and cached forever.

v2.4 Breaking Changes:

  • Renamed Get() → Col() for consistency with database terminology
  • Renamed Fields() → Cols() with []string return type (was []any)
  • All() now returns []string instead of []any
  • Added As() for immutable aliasing: cols.As("u").Col("ID") → "u.id"
  • Added FieldMap() and AllFields() for struct-to-query conversion

type DeleteQueryBuilder added in v0.13.0

type DeleteQueryBuilder interface {
	// Filtering
	Where(filter Filter) DeleteQueryBuilder

	// Batch operations
	Limit(limit uint64) DeleteQueryBuilder
	OrderBy(orderBys ...string) DeleteQueryBuilder

	// SQL generation
	ToSQL() (sql string, args []any, err error)
}

DeleteQueryBuilder defines the interface for DELETE query building with type-safe filtering. This interface wraps squirrel.DeleteBuilder with Filter API support. SECURITY: The string-identifier arguments of OrderBy must be developer-controlled, not user input. They are validated against a safe identifier grammar on ALL vendors BEFORE interpolation; values outside that grammar surface as a ToSQL() error. Route dynamic/computed orderings through qb.Expr()/Raw() (which require an explicit security annotation) and pass user values through the Filter API. See ADR-031.

type Filter added in v0.13.0

type Filter interface {
	squirrel.Sqlizer

	// ToSQL is a convenience method with idiomatic Go naming.
	// It should delegate to ToSql() in implementations.
	ToSQL() (sql string, args []any, err error)
}

Filter represents a composable WHERE clause filter that can be combined with AND/OR/NOT operators. Filters are created through FilterFactory methods obtained from QueryBuilder.Filter().

Filter embeds squirrel.Sqlizer for compatibility with Squirrel's query builder, and adds ToSQL() as a convenience method with idiomatic Go naming (uppercase SQL).

This is an interface to support mocking and testing. The concrete implementation is in database/internal/builder package.

type FilterFactory added in v0.13.0

type FilterFactory interface {
	// Comparison operators
	Eq(column string, value any) Filter
	NotEq(column string, value any) Filter
	Lt(column string, value any) Filter
	Lte(column string, value any) Filter
	Gt(column string, value any) Filter
	Gte(column string, value any) Filter
	In(column string, values any) Filter
	NotIn(column string, values any) Filter
	Like(column, pattern string) Filter
	Null(column string) Filter
	NotNull(column string) Filter
	Between(column string, lowerBound, upperBound any) Filter

	// Regex matching (vendor-specific):
	//   PostgreSQL: ~ (CS), ~* (CI), !~ (NOT CS), !~* (NOT CI)
	//   Oracle:     REGEXP_LIKE(col, pat[, 'i']), optionally negated with NOT
	Regex(column, pattern string) Filter
	RegexI(column, pattern string) Filter
	NotRegex(column, pattern string) Filter
	NotRegexI(column, pattern string) Filter

	// JSONContains tests JSON containment (PostgreSQL @>). Currently
	// PostgreSQL-only; calling on Oracle yields an error filter.
	JSONContains(column string, value any) Filter

	// Logical operators
	And(filters ...Filter) Filter
	Or(filters ...Filter) Filter
	Not(filter Filter) Filter

	// Raw escape hatch
	Raw(condition string, args ...any) Filter

	// Subquery support (v2.1+)
	Exists(subquery SelectQueryBuilder) Filter
	NotExists(subquery SelectQueryBuilder) Filter
	InSubquery(column string, subquery SelectQueryBuilder) Filter
}

FilterFactory provides methods for creating composable, type-safe query filters. Filters maintain vendor-specific quoting rules and can be combined with AND/OR/NOT logic.

type InsertQueryBuilder added in v0.30.0

type InsertQueryBuilder interface {
	// Columns passes names through unchanged. For Oracle reserved-word quoting,
	// use qb.InsertWithColumns or pre-quote via a Columns helper.
	Columns(columns ...string) InsertQueryBuilder

	Values(values ...any) InsertQueryBuilder
	SetMap(clauses map[string]any) InsertQueryBuilder
	Options(options ...string) InsertQueryBuilder
	Prefix(sql string, args ...any) InsertQueryBuilder
	Suffix(sql string, args ...any) InsertQueryBuilder

	// Select returns an error (deferred to ToSQL()) if sb is not the concrete *SelectQueryBuilder from this package.
	Select(sb SelectQueryBuilder) InsertQueryBuilder

	ToSQL() (sql string, args []any, err error)
}

InsertQueryBuilder defines the interface for INSERT query building. Wraps squirrel.InsertBuilder so the public API exposes ToSQL() (S8179) consistently with SelectQueryBuilder/UpdateQueryBuilder/DeleteQueryBuilder. See ADR-017 for the rationale and migration path.

type Interface

type Interface interface {
	// Querier provides core query execution operations
	Querier

	// Transactor provides transaction management operations
	Transactor

	// Prepared statements
	Prepare(ctx context.Context, query string) (Statement, error)

	// Health and diagnostics
	Health(ctx context.Context) error
	Stats() (map[string]any, error)

	// Connection management
	Close() error

	// Migration support
	MigrationTable() string
	CreateMigrationTable(ctx context.Context) error
}

Interface defines the common database operations supported by the framework. This is the main interface that applications and modules should depend on for database operations, allowing for easy mocking and testing.

Interface is composed of focused sub-interfaces following the Single Responsibility Principle:

  • Querier: Core query execution (Query, QueryRow, Exec, DatabaseType)
  • Transactor: Transaction management (Begin, BeginTx)

For easier testing, consider using the smaller interfaces (Querier or Transactor) when your code doesn't need all Interface capabilities. The database/testing package provides utilities for mocking these interfaces with minimal boilerplate.

Backward compatibility: All existing code using Interface continues to work unchanged.

type InvalidAliasError added in v0.60.0

type InvalidAliasError struct {
	// Alias is the refused alias, exactly as it was passed.
	Alias string
}

InvalidAliasError reports an alias argument that the identifier grammar refuses. Columns.As panics with this value rather than returning an error: its signature has no error channel, and an alias is a developer constant, so a violation is a programming error surfaced at construction rather than a deferred query error.

It is a distinct type so a recovery site can report the panic by TYPE without rendering the refused alias (ADR-081), and so a caller can match it:

var invalid *dbtypes.InvalidAliasError
if errors.As(recovered.(error), &invalid) { ... }

func (*InvalidAliasError) Error added in v0.60.0

func (e *InvalidAliasError) Error() string

type JoinFilter added in v0.13.0

type JoinFilter interface {
	squirrel.Sqlizer

	// ToSQL is a convenience method with idiomatic Go naming.
	// It should delegate to ToSql() in implementations.
	ToSQL() (sql string, args []any, err error)
}

JoinFilter represents a filter specifically for JOIN ON conditions where columns are compared to other columns (not to values). This follows Single Responsibility Principle by separating column-to-column comparisons (JOIN) from column-to-value comparisons (WHERE).

JoinFilters are created through JoinFilterFactory methods obtained from QueryBuilder.JoinFilter().

Example:

jf := qb.JoinFilter()
query := qb.Select("*").From("users").JoinOn("profiles", jf.EqColumn("users.id", "profiles.user_id"))

This is an interface to support mocking and testing. The concrete implementation is in database/internal/builder package.

type JoinFilterFactory added in v0.13.0

type JoinFilterFactory interface {
	// Column-to-column comparison operators
	EqColumn(leftColumn, rightColumn string) JoinFilter
	NotEqColumn(leftColumn, rightColumn string) JoinFilter
	LtColumn(leftColumn, rightColumn string) JoinFilter
	LteColumn(leftColumn, rightColumn string) JoinFilter
	GtColumn(leftColumn, rightColumn string) JoinFilter
	GteColumn(leftColumn, rightColumn string) JoinFilter

	// Column-to-value comparison operators (v2.2+)
	// These methods accept any value type, including RawExpression for complex SQL.
	// Regular values generate placeholders; RawExpression values are inserted verbatim.
	Eq(column string, value any) JoinFilter
	NotEq(column string, value any) JoinFilter
	Lt(column string, value any) JoinFilter
	Lte(column string, value any) JoinFilter
	Gt(column string, value any) JoinFilter
	Gte(column string, value any) JoinFilter
	In(column string, values any) JoinFilter
	NotIn(column string, values any) JoinFilter
	Like(column, pattern string) JoinFilter
	Null(column string) JoinFilter
	NotNull(column string) JoinFilter
	Between(column string, lowerBound, upperBound any) JoinFilter

	// Logical operators for complex JOIN conditions
	And(filters ...JoinFilter) JoinFilter
	Or(filters ...JoinFilter) JoinFilter

	// Raw escape hatch for complex JOIN conditions
	Raw(condition string, args ...any) JoinFilter
}

JoinFilterFactory provides methods for creating type-safe JOIN ON filters. JoinFilters support both column-to-column comparisons and column-to-value comparisons, enabling mixed JOIN conditions without requiring Raw() for common cases.

Column-to-value methods accept RawExpression for complex SQL expressions:

jf.Eq("amount", qb.MustExpr("TO_NUMBER('100')"))  // Expression support (no placeholders inside expressions)
jf.Eq("status", "active")                         // Simple value with placeholder

type Querier added in v0.16.0

type Querier interface {
	// Query executes a SQL query that returns rows, typically a SELECT statement.
	// The caller is responsible for closing the returned rows.
	//
	// The query should use vendor-specific placeholders:
	//   - PostgreSQL: $1, $2, $3
	//   - Oracle: :1, :2, :3
	//
	// For vendor-agnostic query construction, use the QueryBuilder.
	Query(ctx context.Context, query string, args ...any) (*sql.Rows, error)

	// QueryRow executes a SQL query that is expected to return at most one row.
	// QueryRow always returns a non-nil value. Errors are deferred until Row's Scan method is called.
	//
	// For vendor-agnostic query construction, use the QueryBuilder.
	QueryRow(ctx context.Context, query string, args ...any) Row

	// Exec executes a SQL statement that doesn't return rows, typically INSERT, UPDATE, or DELETE.
	// The returned sql.Result provides RowsAffected and LastInsertId (if supported by the vendor).
	//
	// For vendor-agnostic query construction, use the QueryBuilder.
	Exec(ctx context.Context, query string, args ...any) (sql.Result, error)

	// DatabaseType returns the vendor identifier for this database connection.
	// Valid values are defined as constants: PostgreSQL, Oracle.
	//
	// This is included in Querier (despite being metadata) because the query builder
	// requires vendor information for placeholder generation and identifier quoting.
	// Including it here prevents forcing all test mocks to implement additional interfaces.
	DatabaseType() string
}

Querier defines the core query execution operations that represent 80% of typical database usage. This interface follows the Single Responsibility Principle by focusing solely on query execution, separate from transaction management, health checks, and migration support.

Querier is designed for easy mocking in unit tests, requiring only 4 methods instead of the full 12 methods in the Interface type. Most business logic only needs query execution capabilities.

The database.Interface type embeds Querier, so all existing code continues to work unchanged.

Usage in tests:

// Simple mock implementation
type mockQuerier struct {
    queryFunc    func(ctx context.Context, query string, args ...any) (*sql.Rows, error)
    queryRowFunc func(ctx context.Context, query string, args ...any) Row
    execFunc     func(ctx context.Context, query string, args ...any) (sql.Result, error)
}

// Inject via ModuleDeps.DB
deps := &app.ModuleDeps{
    DB: func(ctx context.Context) (database.Interface, error) {
        return mockQuerier, nil
    },
}

For comprehensive testing utilities, see the database/testing package which provides TestDB with fluent API for setting up query expectations and assertions.

type QueryBuilderInterface added in v0.8.1

type QueryBuilderInterface interface {
	// Vendor information
	Vendor() string

	// Filter factories
	Filter() FilterFactory
	JoinFilter() JoinFilterFactory

	// Expression builder (v2.1+)
	// Returns error if SQL is empty, too many aliases provided, or alias contains dangerous characters.
	Expr(sql string, alias ...string) (RawExpression, error)

	// MustExpr is like Expr but panics on error.
	// Use this only in static initialization or tests where errors indicate programming bugs.
	MustExpr(sql string, alias ...string) RawExpression

	// Column metadata extraction (v2.4+)
	// Extracts column metadata from structs with `db:"column_name"` tags.
	// Lazily parses struct on first use, caches forever.
	// Returns vendor-specific quoted column names (e.g., Oracle reserved words).
	//
	// Example:
	//   type User struct {
	//       ID    int64  `db:"id"`
	//       Level string `db:"level"` // Oracle reserved word
	//   }
	//   cols := qb.Columns(&User{})
	//   qb.Select(cols.Col("ID"), cols.Col("Level")).From("users")
	//
	//   // With aliasing:
	//   u := cols.As("u")
	//   qb.Select(u.Col("ID"), u.Col("Level")).From(Table("users").As("u"))
	//
	// Panics if structPtr is not a pointer to a struct with db tags.
	Columns(structPtr any) Columns

	// Query builders
	Select(columns ...any) SelectQueryBuilder
	Insert(table string) InsertQueryBuilder
	InsertWithColumns(table string, columns ...string) InsertQueryBuilder

	// Struct-based INSERT (v2.4+)
	// InsertStruct extracts all fields from a struct instance and creates an INSERT query.
	// Zero-value ID fields (int64/string) are automatically excluded to support auto-increment.
	InsertStruct(table string, instance any) InsertQueryBuilder

	// InsertFields extracts only specified fields from a struct instance for INSERT.
	// Useful for partial inserts or when you need explicit control over included fields.
	InsertFields(table string, instance any, fields ...string) InsertQueryBuilder

	Update(table string) UpdateQueryBuilder
	Delete(table string) DeleteQueryBuilder

	// Vendor-specific helpers
	BuildCaseInsensitiveLike(column, value string) squirrel.Sqlizer

	// BuildUpsert requires a non-empty conflictColumns that names no column twice
	// and whose every entry also names a column of insertColumns, on every vendor;
	// a call violating any of those returns an error before any SQL is produced.
	// Both questions — whether two entries are the same column, and whether a
	// conflict column is among the inserted ones — are decided by the column each
	// key NAMES, not by how it is spelled. On Oracle that is the name the database
	// stores: a key the builder emits unquoted folds to upper case, while one it
	// emits quoted (a caller-quoted key, or a reserved word) is taken verbatim. So
	// id, ID and "ID" are one column there — a case variant is a duplicate and also
	// satisfies membership, and so does a quoted spelling of the same name — while
	// "id" is a second column and level and LEVEL stay two, both being quoted.
	// PostgreSQL quotes every identifier, so a case variant is never a duplicate
	// there: it is a second column, a legitimate composite conflict target, and not
	// a match for an insert key spelled differently.
	//
	// insertColumns and updateColumns must each name every column at most once,
	// judged by that same vendor identity rule. A map cannot hold an exact repeat,
	// but two keys can still name one column: on Oracle {"id": 1, "ID": 2} is one
	// column written twice and is rejected, while on PostgreSQL it is two columns
	// and builds. Every key is TRIMMED before it is judged, and the trimmed
	// spelling is what the statement renders, so {"id": 1, " id ": 2} names one
	// column on both vendors and is rejected on both, naming both spellings.
	//
	// ONE acceptance rule decides what a key may be, on every vendor: a single
	// column name — no qualifier, no function call, no empty name — carrying no
	// quote of its own beyond a plain wrapping pair. COUNT(*), t.name, a""b and
	// "a""b" are therefore refused on PostgreSQL as well as on Oracle. An
	// identifier argument carries no escaping beyond the accepted wrapping quote
	// pair; the door quotes. A column whose name
	// genuinely holds a quote has no key form here at all — conflictColumns is
	// []string and both maps are keyed by string, so there is no expression key to
	// pass; such a schema needs a hand-written statement. A column literally named
	// count(*) is unaffected: it stays reachable as the quoted key "count(*)" — PostgreSQL used to refuse every quoted key and now takes them
	// like Oracle, bounded by that same no-interior-quote rule. The rule is Oracle's grammar
	// in origin — its MERGE names conflict and insert keys as column aliases in
	// the USING clause and in the INSERT list, neither of which admits anything
	// else — and PostgreSQL applies it so that one spelling of a column works
	// everywhere the call names it. Oracle update keys become UPDATE SET targets,
	// where Oracle itself would also accept an alias-qualified one; refusing those
	// is this API's restriction, for the same reason.
	//
	// BuildUpsert rejects a column present in both conflictColumns and
	// updateColumns on every vendor, because Oracle's MERGE cannot update a
	// column referenced in its ON clause (ORA-38104). Identity is the same rule the
	// checks above use — the column each key names, so on Oracle conflict "ID" and
	// update id collide, while PostgreSQL keeps every spelling distinct. When its update value equals
	// its insert value, drop it from updateColumns and no column value changes:
	// the conflict match pins it on a matched row and the INSERT supplies it on an
	// unmatched one. Dropping the only update column empties the set, which builds
	// DO NOTHING (and omits Oracle's WHEN MATCHED arm), so a matched row is no
	// longer updated at all: its UPDATE triggers stop firing and RETURNING yields
	// no row. Where that matters keep a genuine non-conflict column, or issue the
	// UPDATE explicitly under the same transaction and locking rule as below.
	// When the two values differ, the call was rewriting the
	// conflict column itself, which no vendor-portable upsert can express — issue
	// a separate UPDATE instead: in the same transaction as the insert, keyed on
	// the conflict columns, and holding the row lock the single statement took for
	// you (SELECT ... FOR UPDATE or equivalent). Splitting one atomic upsert into
	// two statements lets a concurrent writer interleave, and under READ COMMITTED
	// a shared transaction alone does not stop it.
	BuildUpsert(table string, conflictColumns []string, insertColumns, updateColumns map[string]any) (query string, args []any, err error)

	// Database function builders
	BuildCurrentTimestamp() string
	BuildUUIDGeneration() string
	BuildBooleanValue(value bool) any

	// Identifier escaping
	EscapeIdentifier(identifier string) string
}

QueryBuilderInterface defines the interface for vendor-specific SQL query building. This interface allows for dependency injection and mocking of query builders, enabling unit testing of business logic that constructs queries without actually generating SQL strings.

type RawExpression added in v0.14.2

type RawExpression struct {
	SQL   string // The raw SQL expression
	Alias string // Optional alias (AS clause)
}

RawExpression represents a raw SQL expression that can be used in SELECT, GROUP BY, and ORDER BY clauses. It allows using SQL functions, aggregations, calculations, and other expressions that go beyond simple column names.

SECURITY WARNING: Raw SQL expressions are NOT escaped or sanitized by the framework. Never interpolate user input directly into expressions - this creates SQL injection vulnerabilities. Only use static SQL or carefully validated values in expressions.

Safe usage:

expr, err := qb.Expr("COUNT(*)", "total") // Aggregation with alias
if err != nil { return err }
qb.Select(expr)

expr, err = qb.Expr("UPPER(name)") // Function without alias
if err != nil { return err }
qb.Select(expr)

expr, err = qb.Expr("price * quantity", "total") // Calculation with alias
if err != nil { return err }
qb.Select(expr)

Unsafe usage (NEVER do this):

userInput := req.Query("column")
expr, err := qb.Expr(fmt.Sprintf("UPPER(%s)", userInput)) // SQL INJECTION RISK!
if err != nil { return err }
qb.Select(expr)

func Expr added in v0.14.2

func Expr(sql string, alias ...string) (RawExpression, error)

Expr creates a raw SQL expression with optional alias for use in SELECT, GROUP BY, and ORDER BY clauses.

Parameters:

  • sql: The raw SQL expression (e.g., "COUNT(*)", "UPPER(name)", "price * quantity")
  • alias: Optional alias for the expression (e.g., "total", "upper_name"). Max 1 alias allowed.

Returns:

  • RawExpression: The constructed expression
  • error: ErrEmptyExpressionSQL, ErrTooManyAliases, or ErrInvalidAlias on validation failure

Examples:

// Aggregation with alias
expr, err := qb.Expr("COUNT(*)", "total")
if err != nil { return err }

// Function without alias
expr, err := qb.Expr("UPPER(name)")

// Calculation with alias
expr, err := qb.Expr("price * quantity", "line_total")

SECURITY WARNING: Never interpolate user input directly into the sql parameter. This function does NOT sanitize SQL - you are responsible for ensuring safety.

func MustExpr added in v0.19.0

func MustExpr(sql string, alias ...string) RawExpression

MustExpr is like Expr but panics on error. Use this only in static initialization or tests where errors indicate programming bugs.

func (RawExpression) Validate added in v0.60.0

func (e RawExpression) Validate() error

Validate reports why this expression may not be interpolated, or nil.

RawExpression is a plain struct, so a caller can build one directly and never reach Expr(). This is the single funnel both paths share: Expr() calls it at construction, and every builder door that interpolates an expression calls it again at consumption, where a struct literal is indistinguishable from a constructed one (#1153, ADR-082).

The SQL itself is NOT validated — it is the sanctioned raw-SQL escape hatch, and the caller owns its safety. Only its emptiness and the alias are checked. A non-empty alias must be an unquoted identifier under the shared grammar (ADR-031/ADR-082): it is interpolated verbatim after AS, and the substring denylist this replaces accepted everything it did not enumerate — a space, a parenthesis, a newline (#1164).

type Row added in v0.9.0

type Row interface {
	Scan(dest ...any) error
	Err() error
}

Row represents a single result set row with basic scanning behavior.

func NewRowFromSQL added in v0.9.0

func NewRowFromSQL(row *sql.Row) Row

NewRowFromSQL wraps the provided *sql.Row in a Row. If row is nil, NewRowFromSQL returns nil.

type SelectQueryBuilder added in v0.11.0

type SelectQueryBuilder interface {
	// Core SELECT builder methods
	// From accepts either string table names or *TableRef instances with optional aliases.
	// String table identifiers are validated before interpolation (see ADR-031).
	From(from ...any) SelectQueryBuilder

	// Type-safe JOIN methods with JoinFilter (v2.0+)
	// Each accepts either string table name or *TableRef instance with optional alias
	JoinOn(table any, filter JoinFilter) SelectQueryBuilder
	LeftJoinOn(table any, filter JoinFilter) SelectQueryBuilder
	RightJoinOn(table any, filter JoinFilter) SelectQueryBuilder
	InnerJoinOn(table any, filter JoinFilter) SelectQueryBuilder
	CrossJoinOn(table any) SelectQueryBuilder

	GroupBy(groupBys ...any) SelectQueryBuilder

	// Having adds a HAVING predicate. Prefer a RawExpression from qb.Expr() —
	// Having(qb.MustExpr("SUM(amount) > ?"), 100), or qb.Expr when you handle its
	// (RawExpression, error) return — which is the sanctioned path for
	// the aggregate comparisons HAVING exists for; an alias on that expression is
	// an error (ErrAliasInHaving), since a predicate projects nothing. A string
	// predicate is a raw-SQL door on par with f.Raw/jf.Raw/database.Raw and needs
	// the same inline `// SECURITY: Manual SQL review completed - <what was
	// verified>` annotation at every call site. The RawExpression form is exempt
	// for consistency with Select/GroupBy/OrderBy, not because it is safer — its
	// SQL body is never validated and carries identical injection risk. Neither
	// form is checked against the identifier grammar: HAVING takes a predicate,
	// not an identifier (ADR-082).
	Having(pred any, rest ...any) SelectQueryBuilder
	OrderBy(orderBys ...any) SelectQueryBuilder
	Limit(limit uint64) SelectQueryBuilder
	Offset(offset uint64) SelectQueryBuilder
	Paginate(limit, offset uint64) SelectQueryBuilder

	// Composable WHERE clause
	Where(filter Filter) SelectQueryBuilder

	// SQL generation
	ToSQL() (sql string, args []any, err error)
}

SelectQueryBuilder defines the interface for enhanced SELECT query building with type safety. This interface extends basic squirrel.SelectBuilder functionality with additional methods for composable filters, JOIN operations, and vendor-specific query features. SECURITY: Every identifier argument on this builder — From and the JOIN table methods (JoinOn/LeftJoinOn/RightJoinOn/InnerJoinOn/CrossJoinOn), OrderBy, GroupBy, the Select column list, and every Filter/JoinFilter column — must be developer-controlled, not user input. All are validated against a safe identifier grammar on ALL vendors BEFORE interpolation; a value outside that grammar surfaces as a ToSQL() error. Route dynamic or computed expressions through qb.Expr()/MustExpr(). The annotation rule is narrower than that and names four doors: f.Raw(), jf.Raw(), database.Raw() and a STRING predicate passed to Having() each require an inline "// SECURITY: Manual SQL review completed - <rationale>" comment at every call site, because each admits arbitrary SQL rather than an expression the builder still places. qb.Expr() does not carry that requirement — a consistency rule, not a safety claim: Validate() never inspects the SQL body, so an Expr body is raw SQL too, greppable by its own name rather than by an annotation.

Having is in that list for its STRING form only. It takes a predicate rather than an identifier, so no identifier grammar can judge it and a string argument is interpolated as written; Having(qb.MustExpr(...)) is the sanctioned expression form and is not annotated. Prefix, Suffix and Options on the INSERT builder are the same shape, minus the qb.Expr() alternative.

The Filter API parameterizes its VALUES; that is a separate property from validating its COLUMNS, and reading the first as the second is what left the column doors open between ADR-031 and ADR-082. See ADR-031 and ADR-082.

type Statement

type Statement interface {
	// Query execution
	Query(ctx context.Context, args ...any) (*sql.Rows, error)
	QueryRow(ctx context.Context, args ...any) Row
	Exec(ctx context.Context, args ...any) (sql.Result, error)

	// Statement management
	Close() error
}

Statement defines the interface for prepared statements

type TableRef added in v0.14.2

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

TableRef represents a table reference with optional alias for SQL queries. Use the Table() function to create instances.

Example:

table, _ := Table("customers")
aliased, _ := table.As("c")

func MustTable added in v0.19.0

func MustTable(name string) *TableRef

MustTable is like Table but panics on error. Use this only in static initialization or tests where errors indicate programming bugs.

func Table added in v0.14.2

func Table(name string) (*TableRef, error)

Table creates a new table reference. Returns ErrEmptyTableName if name is empty. Call As() to add an alias, or use directly for tables without aliases.

Example:

table, err := Table("users")
if err != nil { return err }
aliased, err := table.As("u")

func (*TableRef) Alias added in v0.14.2

func (t *TableRef) Alias() string

Alias returns the table alias, or empty string if no alias.

func (*TableRef) As added in v0.14.2

func (t *TableRef) As(alias string) (*TableRef, error)

As creates a new TableRef with the given alias. Returns ErrEmptyTableAlias if alias is empty. Returns a new TableRef instance (immutable pattern).

Example:

table, _ := Table("customers")
aliased, err := table.As("c")

func (*TableRef) HasAlias added in v0.14.2

func (t *TableRef) HasAlias() bool

HasAlias returns true if this table has an alias.

func (*TableRef) MustAs added in v0.19.0

func (t *TableRef) MustAs(alias string) *TableRef

MustAs is like As but panics on error. Use this only in static initialization or tests where errors indicate programming bugs.

func (*TableRef) Name added in v0.14.2

func (t *TableRef) Name() string

Name returns the table name (unquoted).

type Transactor added in v0.16.0

type Transactor interface {
	// Begin starts a new transaction with default isolation level.
	// The returned Tx must be committed or rolled back to release resources.
	//
	// Common usage pattern:
	//   tx, err := db.Begin(ctx)
	//   if err != nil { return err }
	//   defer tx.Rollback(ctx)  // No-op if already committed
	//   // ... execute operations on tx ...
	//   return tx.Commit(ctx)
	Begin(ctx context.Context) (Tx, error)

	// BeginTx starts a new transaction with explicit isolation level and read-only settings.
	// Use this when you need precise control over transaction behavior.
	//
	// Common isolation levels (from database/sql):
	//   - sql.LevelDefault: Use database's default isolation
	//   - sql.LevelReadUncommitted: Lowest isolation, allows dirty reads
	//   - sql.LevelReadCommitted: Prevents dirty reads (PostgreSQL default)
	//   - sql.LevelRepeatableRead: Prevents dirty and non-repeatable reads
	//   - sql.LevelSerializable: Highest isolation, full transaction isolation
	//
	// Example (read-only transaction for complex reporting):
	//   tx, err := db.BeginTx(ctx, &sql.TxOptions{
	//       Isolation: sql.LevelRepeatableRead,
	//       ReadOnly:  true,
	//   })
	//
	// Note: Not all databases support all isolation levels. Consult vendor documentation.
	BeginTx(ctx context.Context, opts *sql.TxOptions) (Tx, error)
}

Transactor defines transaction management operations. This interface follows the Single Responsibility Principle by focusing solely on transaction lifecycle, separate from query execution, health checks, and migration support.

Transactor is typically used by business logic that needs to execute multiple operations atomically (all succeed or all fail). For query-only operations, use the Querier interface.

The database.Interface type embeds Transactor, so all existing code continues to work unchanged.

Usage in services:

func (s *OrderService) CreateWithPayment(ctx context.Context, order Order, payment Payment) error {
    db, err := s.deps.DB(ctx)
    if err != nil {
        return err
    }

    tx, err := db.Begin(ctx)
    if err != nil {
        return err
    }
    defer tx.Rollback(ctx)  // No-op if already committed

    if err := s.insertOrder(ctx, tx, order); err != nil {
        return err
    }
    if err := s.insertPayment(ctx, tx, payment); err != nil {
        return err
    }

    return tx.Commit(ctx)
}

For testing transaction logic, see the database/testing package which provides TestTx for tracking commit/rollback behavior and query execution within transactions.

type Tx

type Tx interface {
	// Query execution within transaction
	Query(ctx context.Context, query string, args ...any) (*sql.Rows, error)
	QueryRow(ctx context.Context, query string, args ...any) Row
	Exec(ctx context.Context, query string, args ...any) (sql.Result, error)

	// Prepared statements within transaction
	Prepare(ctx context.Context, query string) (Statement, error)

	// Transaction control
	// Note: Context is passed to support proper cancellation and tracing.
	// The context should be the same one used to begin the transaction.
	Commit(ctx context.Context) error
	Rollback(ctx context.Context) error
}

Tx defines the interface for database transactions

type UpdateQueryBuilder added in v0.13.0

type UpdateQueryBuilder interface {
	// Data modification
	Set(column string, value any) UpdateQueryBuilder
	SetMap(clauses map[string]any) UpdateQueryBuilder

	// Struct-based UPDATE (v2.4+)
	// SetStruct extracts field values from a struct instance for UPDATE.
	// If fields are specified, only those fields are updated.
	// If no fields specified, all struct fields are updated.
	SetStruct(instance any, fields ...string) UpdateQueryBuilder

	// Filtering
	Where(filter Filter) UpdateQueryBuilder

	// SQL generation
	ToSQL() (sql string, args []any, err error)
}

UpdateQueryBuilder defines the interface for UPDATE query building with type-safe filtering. This interface wraps squirrel.UpdateBuilder with Filter API support and vendor-specific quoting. SECURITY: The string-identifier arguments of Set and SetMap (the SET targets) must be developer-controlled, not user input. They are validated against a safe identifier grammar on ALL vendors BEFORE interpolation; values outside that grammar surface as a ToSQL() error. Route dynamic/computed expressions through qb.Expr()/Raw() (which require an explicit security annotation) and pass user values through the value side of Set/SetMap. See ADR-031.

type Vendor added in v0.9.0

type Vendor = string

Database vendor identifiers shared across the database packages.

const (
	PostgreSQL Vendor = "postgresql"
	Oracle     Vendor = "oracle"
)

Jump to

Keyboard shortcuts

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