query

package
v0.96.0 Latest Latest
Warning

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

Go to latest
Published: Nov 7, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package query is part of the GoFastr framework. See https://github.com/DonaldMurillo/gofastr for documentation.

Index

Constants

View Source
const MaxIdentBytes = 63

MaxIdentBytes is the longest identifier Postgres keeps (NAMEDATALEN-1). It cuts a longer one to this length with only a NOTICE.

Variables

This section is empty.

Functions

func FitIdent added in v0.96.0

func FitIdent(name string) string

FitIdent fits a generated object name (an index, a constraint) into MaxIdentBytes. A name that fits is returned as it is. A longer one keeps its first 54 bytes and ends in "_" plus 8 hex digits of the SHA-256 of the whole name, ASCII-folded as unquoted DDL folds it. Postgres would cut it at 63 bytes instead, and two names sharing those bytes named one object: CREATE INDEX IF NOT EXISTS then skipped the second. The name is a validated identifier (ASCII), so the cut never splits a character.

func FitObjectName added in v0.96.0

func FitObjectName(name1, name2, label string) string

FitObjectName names an object this code creates from two parts and a label. A name that fits MaxIdentBytes is name1_name2_label, the name Postgres gives it (ObjectName). A longer one is trimmed the way ObjectName trims it, with 8 hex digits of a SHA-256 of the three parts before the label, so two long names that trim to one prefix stay distinct: x…xa and x…xb (60 x's) both trim to t_x…x_key, where Postgres numbers the second by what already exists and CREATE INDEX IF NOT EXISTS skipped it. The hash covers each part with its length, not the underscore join: t_a + x…xb and t + a_x…xb join to one string, and index names share a schema.

func IndexOn added in v0.96.0

func IndexOn(sqlite bool, name, table string) string

IndexOn renders the "<index> ON <table>" part of a CREATE INDEX for an index named name on table, both already validated and quoted (or both left bare). The index lives in its table's schema, and the two engines spell that on opposite sides: Postgres refuses a schema on the index name and takes it on the table, SQLite takes it on the index name and refuses it on the indexed table. sqlite picks the SQLite spelling. name is the bare index name, as QuoteIndexName returns it; a schema on table moves onto it on SQLite.

IndexOn(false, `"x_idx"`, `"main"."events"`) → "x_idx" ON "main"."events"
IndexOn(true, `"x_idx"`, `"main"."events"`)  → "main"."x_idx" ON "events"

func IsKeyword added in v0.96.0

func IsKeyword(name string) bool

IsKeyword reports whether name, ASCII-folded, is a word that cannot stand bare as a table, column or index name on SQLite or Postgres (order, group, user, end, desc, limit, …). A dotted name is not one; QuoteKeyword reads each part.

func IsPostgres added in v0.86.0

func IsPostgres(db *sql.DB) bool

IsPostgres reports whether the database behind db answers SELECT version() with a PostgreSQL banner. A driver that errors on the probe or answers with anything else (SQLite drivers either fail on version() or return a SQLite banner) reports false, so callers treat "not Postgres" as the SQLite-compatible path.

It is the canonical form of the probe formerly duplicated as core/a2a's detectSQLDialect, framework/outbox's detectDialect, and battery/queue's detectDBDialect; those now map this bool onto their local dialect enums. It is deliberately a single best-effort shot: boot-critical callers that must fail closed on an unreachable database use framework/migrate's retrying probe (detectDialectFailClosed) instead.

func MustIdent

func MustIdent(s string) string

MustIdent is like SafeIdent but panics on invalid identifiers. Use in init/config-time code where the identifier is a hard-coded constant.

func ObjectName added in v0.96.0

func ObjectName(name1, name2, label string) string

ObjectName is the name Postgres gives an object it names itself from two parts and a label, name1_name2_label (makeObjectName): the inline UNIQUE constraint of column c on table t is ObjectName(t, c, "key"). When the whole is past MaxIdentBytes it trims the longer part one byte at a time until it fits, so a short column name survives a long table name and two columns of one table keep distinct names. The parts are validated identifiers (ASCII), so a cut never splits a character. Postgres adds a digit to the label when the name is already taken; ObjectName does not, so two long names can trim to one. FitObjectName names an object this code creates.

func ParseDBTime added in v0.86.0

func ParseDBTime(src any) (time.Time, error)

ParseDBTime converts a database time-column value, as handed to Scan, into a time.Time. It accepts the shapes the Go SQL drivers produce for time columns — time.Time, non-nil *time.Time, and the text layouts SQLite drivers fall back to — and rejects everything else (including a nil *time.Time) with an error.

It replaces the package-local parsers formerly duplicated as framework/outbox's outboxTime/parseOutboxTime and battery/queue's queueTime/parseQueueTime, which differed only in their error prefixes.

func ParseDBTimeString added in v0.86.0

func ParseDBTimeString(raw string) (time.Time, error)

ParseDBTimeString parses the text layouts a SQLite driver may hand back for a time column: RFC3339 (with or without fractional seconds) and the space-separated forms mattn/go-sqlite3 writes. It is the string half of ParseDBTime, extracted for callers that already hold the stored text.

func ProbeSQLiteBindLayout added in v0.86.0

func ProbeSQLiteBindLayout(ctx context.Context, db *sql.DB) string

ProbeSQLiteBindLayout detects the text layout the connected SQLite driver produces when a time.Time is bound as a parameter: the pure driver binds RFC3339Nano, mattn/go-sqlite3 a space-separated form. Callers whose SQL predicates compare stored time text lexicographically use the probed layout as their canonical normalization target, and rows already in it are canonical for this host and skipped, which keeps normalization idempotent on either driver. An unrecognized probe result falls back to RFC3339Nano (a rewrite that binds time.Time values still self-corrects, because the driver formats the bound value).

It replaces the identical package-local probes formerly duplicated as framework/outbox's (Outbox).probeBindLayout and battery/queue's dead (DBQueue).probeBindLayout.

func QuoteFolded added in v0.96.0

func QuoteFolded(s string) string

QuoteFolded quotes a declared identifier the way the schema stores it. AutoMigrate writes its DDL unquoted, so Postgres folds a declared "Widgets" to widgets, and a quoted "Widgets" then names no table. Folding ASCII letters before quoting keeps the quotes (a reserved word such as order still works) and names what the DDL created. SQLite compares identifiers without case, so the folded spelling names the same table there. Each dot-separated part is quoted on its own. The caller validates the identifier first (via SafeIdent), so it is ASCII.

QuoteFolded("Widgets")        → "widgets"
QuoteFolded("app.UserEvents") → "app"."userevents"

func QuoteIdent

func QuoteIdent(s string) string

QuoteIdent wraps an identifier in double-quotes with internal quotes escaped. The caller is responsible for validating the identifier first (via SafeIdent).

QuoteIdent("users")       → "users"
QuoteIdent(`weird"name`)  → "weird""name"

func QuoteIndexName added in v0.96.0

func QuoteIndexName(s string) string

QuoteIndexName quotes the name of an index or trigger built from a table name. Postgres puts an index in its table's schema and refuses a schema-qualified name, so only the part after the last dot is kept, folded the way QuoteFolded folds it and fitted to 63 bytes (FitIdent), so two long names that share their first 63 bytes stay two indexes.

QuoteIndexName("app.UserEvents_idx") → "userevents_idx"

func QuoteKeyword added in v0.96.0

func QuoteKeyword(name string) string

QuoteKeyword renders an identifier the caller has validated (SafeIdent) for a statement: each dot-separated part that is a keyword quoted and folded (QuoteFolded), every other part bare. It is the spelling the migrator writes its DDL in, so a query that names a column through it names the column the DDL created. A part already quoted is no keyword, so QuoteKeyword of its own output changes nothing.

QuoteKeyword("title")       → title
QuoteKeyword("order")       → "order"
QuoteKeyword("posts.Group") → posts."group"

func QuoteKeywords added in v0.96.0

func QuoteKeywords(names []string) []string

QuoteKeywords is QuoteKeyword of each name, in a new slice.

func RefuseCaseKeptTables added in v0.96.0

func RefuseCaseKeptTables(ctx context.Context, db RowQueryer, tables ...string) error

RefuseCaseKeptTables stops a Postgres store from booting past a table an older build made. Those builds quoted a configured name whole with QuoteIdent, so "Jobs" kept its case and "app.Jobs" became one table literally named app.Jobs. Stores now quote with QuoteFolded and name jobs, or jobs in schema app; booting on would start an empty table and leave every row in the old one. The error carries the statements that move it. A name QuoteIdent and QuoteFolded spell alike is skipped, so a lowercase table costs no query. Call it on Postgres only: SQLite compares names without case. tables are SafeIdent-validated names.

func ReservedIdent added in v0.86.0

func ReservedIdent(name string) bool

ReservedIdent reports whether name, case-insensitively, is a SQL reserved word or common system-table name that must not be used as a bare table name. It replaces the reserved-word checks formerly inlined in the package-local safeIdent validators of core/middleware, core/featureflag, and battery/webhook.

func SQLitePragma added in v0.96.0

func SQLitePragma(fn, table string) string

SQLitePragma renders PRAGMA fn(table) for a table already validated and quoted (or left bare). SQLite takes a schema on the pragma, PRAGMA "main".table_info("events"), and refuses one inside the parentheses.

SQLitePragma("table_info", `"main"."events"`) → PRAGMA "main".table_info("events")

func SafeIdent

func SafeIdent(s string) (string, error)

SafeIdent validates that s is a safe SQL identifier and returns it unchanged (QuoteIdent adds the quotes). This prevents SQL injection when table or column names must be interpolated into queries (they can't be parameterized with $1 placeholders).

Returns an error if s contains characters outside [a-zA-Z0-9_.].

func SafeQuote

func SafeQuote(s string) (string, error)

SafeQuote validates and quotes a SQL identifier in one step.

func SafeTableName added in v0.86.0

func SafeTableName(name string) bool

SafeTableName reports whether name is acceptable as a bare (single- segment) table name: it must pass SafeIdent's character class and leading-letter rule, contain no dot (schema.table is not a bare name), be at most 64 bytes, and not be ReservedIdent.

It replaces the package-local safeIdent validators formerly duplicated in core/middleware (idempotency store), core/featureflag (SQL store), and battery/webhook (subscriber/delivery/inbound stores). The webhook validator was weaker (no leading-character rule, no reserved words); all three now share this one contract.

func Transaction

func Transaction(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) error

Transaction executes fn inside a database transaction. It begins a transaction, calls fn, and commits on success or rolls back on error.

Types

type CountBuilder

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

CountBuilder builds a SELECT COUNT(*) query with parameterized placeholders.

func Count

func Count(table string) *CountBuilder

Count creates a new CountBuilder for the given table. wheres/args are pre-capped. See query.Select's rationale (CRUD List adds ~4-5 Where clauses per count + data builder; without the hint each grows through 1 → 2 → 4 → 8 across the request).

func (*CountBuilder) Build

func (cb *CountBuilder) Build() (string, []any)

Build produces the final parameterized SQL and argument slice.

func (*CountBuilder) Where

func (cb *CountBuilder) Where(condition string, args ...any) *CountBuilder

Where appends a WHERE condition (ANDed with previous conditions).

type DeleteBuilder

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

DeleteBuilder builds a DELETE query with parameterized placeholders.

func Delete

func Delete(table string) *DeleteBuilder

Delete creates a new DeleteBuilder for the given table.

func (*DeleteBuilder) Build

func (db *DeleteBuilder) Build() (string, []any)

Build produces the final parameterized SQL and argument slice.

func (*DeleteBuilder) Matching added in v0.94.0

func (db *DeleteBuilder) Matching(cols ...string) (string, []any)

Matching builds the SELECT of cols from the rows this DELETE would remove: the same table and WHERE conditions, with their arguments.

func (*DeleteBuilder) Returning added in v0.94.0

func (db *DeleteBuilder) Returning(cols ...string) *DeleteBuilder

Returning adds a RETURNING clause naming the deleted rows' columns.

func (*DeleteBuilder) Where

func (db *DeleteBuilder) Where(condition string, args ...any) *DeleteBuilder

Where appends a WHERE condition (ANDed with previous conditions).

type InsertBuilder

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

InsertBuilder builds an INSERT query with parameterized placeholders.

func Insert

func Insert(table string) *InsertBuilder

Insert creates a new InsertBuilder for the given table.

func (*InsertBuilder) Build

func (ib *InsertBuilder) Build() (string, []any)

Build produces the final parameterized SQL and argument slice.

func (*InsertBuilder) Columns

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

Columns sets the columns to insert into.

func (*InsertBuilder) Returning

func (ib *InsertBuilder) Returning(cols ...string) *InsertBuilder

Returning adds a RETURNING clause.

func (*InsertBuilder) Values

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

Values sets the values to insert.

type QueryBuilder

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

QueryBuilder builds a SELECT query with parameterized placeholders.

func Select

func Select(columns ...string) *QueryBuilder

Select creates a new QueryBuilder selecting the given columns.

wheres/args are pre-capped (defaultWhereCap / defaultArgCap) because the CRUD List handler adds ~4-5 WHERE clauses per query × 2 queries (count + data). Starting at nil forces repeated slice growth (1 → 2 → 4 → 8) for the common case; a small capacity hint lets the runtime allocate once at the expected size.

func (*QueryBuilder) Build

func (qb *QueryBuilder) Build() (string, []any)

Build produces the final parameterized SQL and argument slice. It does not mutate the QueryBuilder: safe to call multiple times.

func (*QueryBuilder) Cursor

func (qb *QueryBuilder) Cursor(field string, value any, dir string) *QueryBuilder

Cursor adds keyset/cursor-based pagination. dir "forward" → WHERE field > value, dir "backward" → WHERE field < value.

func (*QueryBuilder) From

func (qb *QueryBuilder) From(table string) *QueryBuilder

From sets the table to query.

func (*QueryBuilder) Join

func (qb *QueryBuilder) Join(table, on string) *QueryBuilder

Join adds an INNER JOIN clause.

func (*QueryBuilder) LeftJoin

func (qb *QueryBuilder) LeftJoin(table, on string) *QueryBuilder

LeftJoin adds a LEFT JOIN clause.

func (*QueryBuilder) Limit

func (qb *QueryBuilder) Limit(n int) *QueryBuilder

Limit sets the LIMIT clause.

func (*QueryBuilder) Offset

func (qb *QueryBuilder) Offset(n int) *QueryBuilder

Offset sets the OFFSET clause.

func (*QueryBuilder) OrWhere

func (qb *QueryBuilder) OrWhere(condition string, args ...any) *QueryBuilder

OrWhere appends a WHERE condition (ORed with previous conditions, taken together: Where(a).Where(b).OrWhere(c) builds (a) AND (b) OR (c), which SQL reads as (a AND b) OR c).

func (*QueryBuilder) Order

func (qb *QueryBuilder) Order(column string, dir string) *QueryBuilder

Order adds an ORDER BY clause.

func (*QueryBuilder) Where

func (qb *QueryBuilder) Where(condition string, args ...any) *QueryBuilder

Where appends a WHERE condition (ANDed with previous conditions). After an OrWhere it ANDs with the whole OR group: Where(a).OrWhere(b).Where(c) builds ((a) OR (b)) AND (c).

type RowQueryer added in v0.96.0

type RowQueryer interface {
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}

RowQueryer is the slice of *sql.DB, *sql.Conn and *sql.Tx that RefuseCaseKeptTables needs.

type StoredTime added in v0.96.0

type StoredTime struct {
	Col string
	Raw any
}

StoredTime is one time column of a row as read back with CAST(col AS TEXT): its name and the driver's raw value (nil for NULL).

type TimeFix added in v0.96.0

type TimeFix struct {
	Col  string
	Read string
	At   time.Time
}

TimeFix is a time column to rewrite: the stored text it was read with and the instant that text parses to, in UTC.

func SQLiteTimeFixes added in v0.96.0

func SQLiteTimeFixes(layout string, cols []StoredTime) ([]TimeFix, error)

SQLiteTimeFixes returns the columns whose stored text is not already in layout (from ProbeSQLiteBindLayout). A NULL column produces no fix, and a canonical value parses and formats back to the same text, so it is skipped and a normalization pass is idempotent. A value that is not text, or that ParseDBTime cannot read, is an error: it is corrupt, and keeping it would leave the bug in place. The caller binds At as a time.Time, so the driver writes exactly what its own binds compare against, and guards the write on Read so a value rewritten since the read is kept.

type UpdateBuilder

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

UpdateBuilder builds an UPDATE query with parameterized placeholders.

func Update

func Update(table string) *UpdateBuilder

Update creates a new UpdateBuilder for the given table.

func (*UpdateBuilder) Build

func (ub *UpdateBuilder) Build() (string, []any)

args follow the placeholders, every SET value before every WHERE arg, whatever order Set and Where were called in.

func (*UpdateBuilder) Matching added in v0.94.0

func (ub *UpdateBuilder) Matching(cols ...string) (string, []any)

Build produces the final parameterized SQL and argument slice. The Matching builds the SELECT of cols from the rows this UPDATE would change: the same table and WHERE conditions, with only their arguments.

func (*UpdateBuilder) Returning

func (ub *UpdateBuilder) Returning(cols ...string) *UpdateBuilder

Returning adds a RETURNING clause.

func (*UpdateBuilder) Set

func (ub *UpdateBuilder) Set(column string, value any) *UpdateBuilder

Set adds a column = value assignment.

func (*UpdateBuilder) Where

func (ub *UpdateBuilder) Where(condition string, args ...any) *UpdateBuilder

Where appends a WHERE condition (ANDed with previous conditions).

Jump to

Keyboard shortcuts

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