Documentation
¶
Overview ¶
Package structbind derives columns, bindings and assignments from a struct's tags, so a table's column list is written once instead of once per statement.
It exists for two reasons, and the second is the more interesting.
The first is convenience. Declaring a table's columns and then repeating them in a select list, a scan list and an insert is three lists that have to stay in step, and other builders remove that with reflection. This does the same.
The second is that it is a test of the library's extension points. Nothing here is privileged: it implements stmt.Binding, which is an interface with no unexported methods, and builds columns through expr and render like any other package. If a reflection-driven binder could not be written outside the core, the claim that a community package can replace the result strategy would be false, and this package is what says otherwise.
The cost is stated plainly because it is the whole trade. Column names come from struct tags, so a typo in a tag is a runtime error where a typo in an expr.Col declaration is a compile error, and the Go type of a field is never checked against the column's. Code generation over github.com/ubgo/sqlb/schema gives the same convenience without giving that up, and is the better answer where a schema exists. This is for the case where one does not.
Index ¶
Constants ¶
const TagKey = "db"
TagKey is the struct tag this package reads. It matches the convention most Go database packages use, so a struct already annotated for one of them works here unchanged.
Variables ¶
var ErrDuplicateColumn = errors.New("structbind: two fields resolve to the same column")
ErrDuplicateColumn is returned when two fields resolve to one column name.
It is an error rather than a last-one-wins because the failure is otherwise invisible. Two fields mapping to "id" render SELECT "id", "id", which every engine accepts; both scan targets then receive the same value and one field silently holds the wrong data, with no error at any layer. A struct with an ID and an Id field does it, and so does a db tag that happens to match a name derived from another field.
The error names both Go fields, because the column name alone does not say which two collided in a struct with embedded types.
var ErrEmptyColumn = errors.New("structbind: namer produced an empty column name")
ErrEmptyColumn is returned when a Namer produces an empty column name.
Only a custom Namer can cause this, since an empty tag falls through to the Namer and every Namer here returns a non-empty name for a non-empty field. It is caught because the alternative renders "u"."" , which is a valid identifier that no table has, so the failure would surface as a confusing error from the engine rather than as the bug it is.
var ErrNoColumns = errors.New("structbind: struct has no exported fields with a db tag")
ErrNoColumns is returned when a struct declares no usable fields, which is always a mistake: it would produce a statement selecting nothing.
var ErrNotAStruct = errors.New("structbind: type parameter must be a struct")
ErrNotAStruct is returned when the type parameter is not a struct.
Functions ¶
func Camel ¶
Camel converts a Go field name to camelCase: createdAt, userId.
For a schema whose columns are camelCase, which is what JavaScript-side tooling generates. The same word splitting as Snake, so HTTPStatus is httpStatus rather than hTTPStatus.
func Exact ¶
Exact uses the Go field name verbatim: CreatedAt.
For a schema whose columns are the Go spelling, which happens where the tables were generated from these structs. Every dialect here quotes identifiers, so a mixed-case column survives.
func Lower ¶
Lower lowercases the whole field name: createdat.
This was the behaviour before a Namer could be chosen, and it exists so that code relying on it can say so rather than depend on a default. It is almost never what a schema looks like; it is here to be named, not to be used.
func Pascal ¶
Pascal converts a Go field name to PascalCase: CreatedAt, UserId.
Distinct from Exact for exactly the reason Camel is distinct from it: splitting normalises a capital run, so HTTPStatus becomes HttpStatus rather than being passed through unchanged. Use Exact when the column really is the Go spelling.
func Snake ¶
Snake converts a Go field name to snake_case, and is the default.
It is the default because it is the convention SQL itself uses and the one most schemas follow, but it is only a default: a column genuinely named "createdAt" is common in schemas generated by JavaScript tooling, which is why Camel exists and why any func is accepted.
A run of capitals is one word, so HTTPStatus is http_status rather than h_t_t_p_status, and a trailing lowercase s belongs to the run before it, so URLs is urls rather than ur_ls. Both rules exist because the alternative produced names no database has ever had.
It has no list of known initialisms, deliberately. Such a list is a decision about somebody else's vocabulary that can never be complete, and it would make the output depend on whether a word happened to be in it. OAuth2Token becomes o_auth2_token rather than oauth2_token; when the derivation is wrong the answer is a db tag, which is exact and local and already the documented escape hatch.
Types ¶
type Mapping ¶
type Mapping[T any] struct { // contains filtered or unexported fields }
Mapping is the column list derived from a struct, bound to one table.
It is built once and shared. Nothing in it is mutable and the reflection all happens in New, so a Mapping is safe to keep in a package-level variable and use from any number of goroutines, which is the point of separating it from the per-statement calls below.
func MustNew ¶
MustNew is New for a package-level variable, where an error can only be a programming mistake and there is nobody to return it to.
func New ¶
New derives a Mapping for T against table t.
Fields are included when they carry a db tag, or when they are exported and the tag is absent, in which case the column comes from the Namer. A tag of "-" excludes a field, which is how a struct carries something that is not a column.
The optional Namer sets how an untagged field name becomes a column name. The default is Snake, so CreatedAt is created_at; pass Camel for a schema whose columns are createdAt, Exact for one whose columns are the Go spelling, or any func for anything else. A tag always wins over the Namer.
Only the first Namer is used. It is variadic so that omitting it is the ordinary call rather than a nil argument, and because a second one could only mean that two parts of a program disagree about the schema.
func (*Mapping[T]) Assignments ¶
func (m *Mapping[T]) Assignments(src *T) []stmt.Assignment
Assignments returns one assignment per column, reading from src, for an INSERT row or an UPDATE SET list.
func (*Mapping[T]) Bindings ¶
Bindings returns one binding per column, pointing into dst.
The pointers are into the struct the caller passed, so everything the binding API documents about concurrency applies: the query holds this memory, and two goroutines executing it write through the same fields.
func (*Mapping[T]) Exprs ¶
Exprs returns the columns as select items, for a statement that scans on its own or does not scan at all.
func (*Mapping[T]) Reader ¶
Reader returns a scan.Reader that produces a fresh T per row.
This is the counterpart to Bindings and the difference is who owns the memory. Bindings point into a struct the caller keeps, so a query holding them is not safe to share and reading many rows means copying the struct inside the loop. A Reader allocates per row and returns values, so scan.All gives a []T directly and one Mapping may be read by any number of goroutines at once.
Prefer this for reads unless you specifically want the caller-owned struct, which is the shape exec.One and the binding API are built around.
type Namer ¶
Namer derives a column name from a Go field name.
It is the fallback for a field with no db tag, never an override: a tag is what somebody wrote down deliberately, so it always wins.
This is a parameter rather than a package-level setting on purpose. Other mappers put the equivalent on the handle as a package-wide function, which makes the meaning of a struct depend on mutable state somewhere else in the process; two libraries sharing a handle then fight over it, and the loser's columns are wrong at runtime with nothing to point at. Passing it to New keeps one Mapping's answer knowable from one call site.