Documentation
¶
Overview ¶
Package query is part of the GoFastr framework. See https://github.com/DonaldMurillo/gofastr for documentation.
Index ¶
- Constants
- func FitIdent(name string) string
- func FitObjectName(name1, name2, label string) string
- func IndexOn(sqlite bool, name, table string) string
- func IsKeyword(name string) bool
- func IsPostgres(db *sql.DB) bool
- func MustIdent(s string) string
- func ObjectName(name1, name2, label string) string
- func ParseDBTime(src any) (time.Time, error)
- func ParseDBTimeString(raw string) (time.Time, error)
- func ProbeSQLiteBindLayout(ctx context.Context, db *sql.DB) string
- func QuoteFolded(s string) string
- func QuoteIdent(s string) string
- func QuoteIndexName(s string) string
- func QuoteKeyword(name string) string
- func QuoteKeywords(names []string) []string
- func RefuseCaseKeptTables(ctx context.Context, db RowQueryer, tables ...string) error
- func ReservedIdent(name string) bool
- func SQLitePragma(fn, table string) string
- func SafeIdent(s string) (string, error)
- func SafeQuote(s string) (string, error)
- func SafeTableName(name string) bool
- func Transaction(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) error
- type CountBuilder
- type DeleteBuilder
- type InsertBuilder
- type QueryBuilder
- func (qb *QueryBuilder) Build() (string, []any)
- func (qb *QueryBuilder) Cursor(field string, value any, dir string) *QueryBuilder
- func (qb *QueryBuilder) From(table string) *QueryBuilder
- func (qb *QueryBuilder) Join(table, on string) *QueryBuilder
- func (qb *QueryBuilder) LeftJoin(table, on string) *QueryBuilder
- func (qb *QueryBuilder) Limit(n int) *QueryBuilder
- func (qb *QueryBuilder) Offset(n int) *QueryBuilder
- func (qb *QueryBuilder) OrWhere(condition string, args ...any) *QueryBuilder
- func (qb *QueryBuilder) Order(column string, dir string) *QueryBuilder
- func (qb *QueryBuilder) Where(condition string, args ...any) *QueryBuilder
- type RowQueryer
- type StoredTime
- type TimeFix
- type UpdateBuilder
- func (ub *UpdateBuilder) Build() (string, []any)
- func (ub *UpdateBuilder) Matching(cols ...string) (string, []any)
- func (ub *UpdateBuilder) Returning(cols ...string) *UpdateBuilder
- func (ub *UpdateBuilder) Set(column string, value any) *UpdateBuilder
- func (ub *UpdateBuilder) Where(condition string, args ...any) *UpdateBuilder
Constants ¶
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
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
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
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
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
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 ¶
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
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
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
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
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
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 ¶
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
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
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
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
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
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 ¶
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 SafeTableName ¶ added in v0.86.0
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.
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
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
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).