Documentation
¶
Overview ¶
Package querygen emits sqlc input for tables shaped the way this module's row conventions expect, in the dialect of whichever of the three databases this module supports will run it.
The conventions are already load-bearing elsewhere. filtering.QueryFilter is a window over created_at and last_updated_at, a cursor compared against id, and a flag deciding whether archived_at rows count. search/sync's Scanner wants a strictly ordered page of IDs and nothing else. database's soft delete is archived_at rather than DELETE. None of that was ever written down as SQL, so each consumer wrote the SQL themselves, once per table, and the conventions held exactly as long as everyone remembered them — which is to say they held until the first table where someone did not.
This package writes that SQL. It is a generator, not a runtime builder: the whole reason to hand queries to sqlc is that they are checked against the schema at build time and come back as typed Go, and a builder that assembles SQL at runtime gives both of those up. What comes out of here is text, to be written to a .sql file and fed to sqlc alongside the schema.
What a caller supplies ¶
A dialect, through For, which returns the Generator every emitter hangs off:
queries := querygen.For(dialect.Postgres).StandardCRUD("widgets", columns)
Then a table name and its column list, in the order the emitted SELECTs should list them. Everything else is read off the column set:
created_at present → the created_after/created_before window
last_updated_at present → the updated_after/updated_before window
archived_at present → soft delete, and the include_archived toggle
last_indexed_at present → the reindex scan search/sync reads through, and
the bulk stamp that maintains it
id → required; the cursor and every query's key
A query whose column is absent is not emitted, and a predicate whose column is absent is not rendered. That is the point of deriving them: a table without last_updated_at cannot end up with an Update that sets it, and a table with archived_at cannot end up without an Archive.
last_indexed_at is the one that took two rounds to get right. Its presence has always decided the reindex scan, and the column has always been database-owned — excluded from the create and the update, so no caller can supply it. What was missing was anything that wrote it: the scan walked a column the convention forbade everyone from maintaining. MarkXAsIndexed is that write, emitted from the same column list as the scan, and a searchsync.Syncer flushes ids into it through searchsync.NewStampBuffer. The column, the query that reads it, and the write that maintains it are one feature rather than three-quarters of one.
WithOmitted subtracts from that set, for a table whose rows are not addressable the way it assumes — a child row written with its parent and never read on its own. It cannot add: what comes out stays a subset of what the columns justify, so the properties above survive a caller who reaches for it.
Two things a column list cannot say are said with options rather than guessed at. WithNullable names the columns a write may set to NULL, which lives in the schema this package never reads; WithDatabaseOwned and WithImmutable name the columns a caller may not assign, which lives in the application. Guessing either produces SQL that generates, compiles, and is wrong at runtime.
Argument names ¶
The emitted SQL binds sqlc arguments whose names are neither the Go field names nor the query-parameter names. All three spellings exist and none of them can be guessed from another, so they are written down here:
filtering.QueryFilter URL parameter sqlc argument CreatedAfter createdAfter created_after CreatedBefore createdBefore created_before UpdatedAfter updatedAfter updated_after UpdatedBefore updatedBefore updated_before IncludeArchived includeArchived include_archived Cursor cursor cursor MaxResponseSize limit result_limit
The bulk stamp binds one argument that is not a filter field at all: ids, the list of row ids to mark as indexed.
include_archived actually includes archived rows ¶
A filtered list's WHERE clause is FilterConditions in its entirety, not an addendum bolted onto a WHERE the caller opened with archived_at IS NULL. The distinction is the difference between a working toggle and a decorative one: a query reading
WHERE t.archived_at IS NULL AND (NOT COALESCE(sqlc.narg(include_archived), false) OR t.archived_at IS NULL)
parses, runs, reports no error, and returns the same rows for either value of the flag, because the first predicate has already decided. Owning the whole clause is what makes that unrepresentable.
The three dialects ¶
Postgres, MySQL and SQLite each get SQL their own server parses, and the difference is confined to five expressions: the case-insensitive substring match, the byte-ordered comparison the reindex scan walks, the sentinel an unset time bound coalesces to, the nullable boolean the archived toggle binds, and the set membership the bulk stamp keys on. They live together in generator.go, as unexported methods, so that what this package assumes about a server is one screen rather than a grep for casts. Everything else — the statement shapes, the query names, which queries a column list justifies — is the same text on all three.
The set is closed at the type. For takes a dialect.Dialect and rejects one outside dialect.Valid rather than emitting a plausible default, and the dialect binds to the Generator rather than to each call, so a Postgres fragment cannot be spliced into a MySQL statement. That matters more than it sounds: the failures are asymmetric. COLLATE "C" in MySQL is a parse error, which is the good case; ILIKE has no SQLite spelling at all, and the substitute folds a narrower set of characters, which is a search that quietly misses rows.
What a consumer sees is one set of sqlc methods with one set of signatures whichever dialect generated them, so the application code above them is written once. Two exceptions, both from sqlc's own inference rather than from anything here: the archived toggle carries a ::boolean on Postgres and cannot elsewhere, because MySQL and SQLite have no boolean type to cast to; and the bulk stamp's id set is a bound array on Postgres and a sqlc.slice expansion on the other two, which changes what reaches the server and not the []string a caller passes.
What each dialect asks of a schema ¶
A table generated for SQLite has to store its timestamps the way SQLite's own CURRENT_TIMESTAMP writes them — YYYY-MM-DD HH:MM:SS, UTC. SQLite has no date type, so the filter window's comparisons are lexicographic over text, and text in any other shape compares in an order that is not chronological. The other two have real timestamp types and no such requirement.
A table generated for MySQL needs its id column to be something MySQL will index as a key: TEXT cannot be a primary key there without a prefix length, so ids belong in a VARCHAR. Nothing in this package enforces either of these; both are schema decisions, and this package never reads the schema.
The one place a dialect changes a signature ¶
Everything above is a difference in SQL under a Go API that does not move. LIMIT is the exception, and it is worth knowing about before choosing MySQL.
Postgres and SQLite take an expression after LIMIT, so an absent page size coalesces to filtering.DefaultQueryFilterLimit and the generated parameter is a pointer a caller may leave nil. MySQL takes an integer literal or a placeholder and nothing else — COALESCE there is a parse error rather than a slower plan — so its LIMIT binds the size and the generated parameter is a value. Leveling the other two down to match would take a working default away from the dialects that can express one in order to make a limitation uniform, which is the wrong way round.
Nothing drifts by leaving them different: the default is filtering's constant rather than a number written here, so the SQL and filtering.QueryFilter.Normalize read the same one. What a MySQL consumer owes its queries is that Normalize call — it turns an absent or zero page size into that constant and clamps an oversized one, the same treatment the URL parameter gets. A MySQL query handed a zero returns no rows, which is loud, rather than a page of some other size.
Index ¶
- Constants
- Variables
- func ForInsert(columns []string, exceptions ...string) []string
- func ForUpdate(columns []string, exceptions ...string) []string
- func Qualify(table, column string) string
- func QualifyAll(table string, columns []string) []string
- func RenderFile(queries []*Query) string
- type Generator
- func (g *Generator) ContainsCondition(column, argument string) string
- func (g *Generator) CursorCondition(table string) string
- func (g *Generator) CursorLimitClause(table string) string
- func (g *Generator) CursorPaginationFragment(table string) string
- func (g *Generator) Dialect() dialect.Dialect
- func (g *Generator) FilterConditions(table string, columns []string, conditions ...string) string
- func (g *Generator) FilterCountSelect(table string, columns, joins []string, conditions ...string) string
- func (g *Generator) IndexStampQuery(table string) string
- func (g *Generator) ReindexScanQuery(table string) string
- func (g *Generator) StandardCRUD(table string, columns []string, opts ...Option) []*Query
- func (g *Generator) TotalCountSelect(table string, columns, joins []string, conditions ...string) string
- type JoinStatement
- type Option
- func WithDatabaseOwned(columns ...string) Option
- func WithEntity(singular, plural string) Option
- func WithImmutable(columns ...string) Option
- func WithNullable(columns ...string) Option
- func WithOmitted(queries ...StandardQuery) Option
- func WithOwnership(column string) Option
- func WithQueryName(query StandardQuery, name string) Option
- type Query
- type QueryAnnotation
- type QueryType
- type StandardQuery
Examples ¶
Constants ¶
const ( // IDColumn is the primary key, and also the pagination cursor. Both roles // require it to sort by creation time — an xid or a ULID, not a serial and // not a UUIDv4 — because a keyset walk over an id that does not sort that // way pages in an order nobody asked for. IDColumn = "id" // CreatedAtColumn carries the row's creation time and bounds the // created_after/created_before window. CreatedAtColumn = "created_at" // LastUpdatedAtColumn is NULL until the row is first updated, which is why // every predicate over it admits NULL explicitly. LastUpdatedAtColumn = "last_updated_at" // ArchivedAtColumn is the soft delete. Rows are archived rather than // deleted, so every read filters on it and no write removes a row. ArchivedAtColumn = "archived_at" // LastIndexedAtColumn records when a row was last written to a search // index. Its presence is what marks a table as one search/sync mirrors, // and it brings two statements with it: the scan a reindex walks, and the // bulk stamp that maintains the column — see IndexStampQuery. LastIndexedAtColumn = "last_indexed_at" // BelongsToAccountColumn is the conventional owner of a tenant-scoped row. // It is a name, not a behavior: scoping queries by it is WithOwnership's // job, because whether a table's rows are readable across accounts is a // decision about that table and not something to infer from a column. BelongsToAccountColumn = "belongs_to_account" )
The columns this module has opinions about. A table is free to hold any others it likes; these are the ones whose presence changes what gets emitted, and whose names are spelled here rather than in each generator so that a table calling its soft-delete column something else is a table this package does not claim to serve.
const ( CursorArg = "cursor" LimitArg = "result_limit" IncludeArchivedArg = "include_archived" CreatedAfterArg = "created_after" CreatedBeforeArg = "created_before" UpdatedAfterArg = "updated_after" UpdatedBeforeArg = "updated_before" )
The sqlc argument names the emitted queries bind. They are the SQL-side spelling of filtering.QueryFilter — see the package comment for the mapping between these, the struct fields, and the URL parameters.
const IDsArg = "ids"
IDsArg is the sqlc argument the bulk stamp binds its id list through. It is not one of the filter arguments above — nothing in filtering.QueryFilter takes a set of ids — so it is spelled separately rather than smuggled into their block.
const NowExpression = "CURRENT_TIMESTAMP"
NowExpression is how the emitted SQL asks for the current time.
The server's clock, never the application's. A row's created_at and a filter's created_after are compared against each other, so they have to come from the same clock; two application instances whose clocks differ by a second would otherwise write rows that a window excludes at random.
It is a constant rather than a Generator method because all three dialects accept the standard spelling: Postgres and MySQL both treat CURRENT_TIMESTAMP as the same function they spell NOW(), and SQLite has only this one. Arithmetic on it is where they part company — see Generator.timeHorizon.
Variables ¶
var ErrDuplicateQueryName = platformerrors.New("two standard queries share a name")
ErrDuplicateQueryName indicates two emitted queries sharing a name. sqlc turns a query name into a Go method name across a whole package, so a duplicate is a compile error in generated code, reported against a file nobody wrote.
var ErrMissingIDColumn = platformerrors.New("column set has no id column")
ErrMissingIDColumn indicates a column set without an id. Every query StandardCRUD emits keys on it, and the cursor walk orders by it, so there is nothing useful to emit for a table that has none.
Functions ¶
func ForInsert ¶
ForInsert returns the columns an INSERT takes values for: everything but the database-owned ones, and anything else the caller names.
Order is preserved, because an INSERT's column list and its VALUES list are positional and have to be rendered from the same slice.
func ForUpdate ¶
ForUpdate returns the columns an UPDATE assigns: ForInsert's set, less the id.
The id is excluded because an UPDATE keys on it. A SET that assigns the column the WHERE matches on is a row that changes its own identity mid-statement, which is legal SQL and never what anyone meant.
func QualifyAll ¶
QualifyAll renders every column as table.column, preserving order.
func RenderFile ¶
RenderFile assembles queries into the byte-exact contents of one sqlc input file: each rendered query, one blank line between them, one trailing newline.
Trailing whitespace is stripped from every line. That is not cosmetic. A generator is usually run twice — once to write the files and once, in CI, to check that the committed files still match — and the check is a byte comparison. Composing fragments into a statement is exactly the operation that leaves a stray space at the end of a line, so normalizing here is what keeps the check answering a question about SQL rather than about whitespace.
Example ¶
RenderFile produces the bytes a .sql file holds, which is what a generator writes and what its --check mode compares against.
package main
import (
"fmt"
"strings"
"github.com/primandproper/platform-go/v12/database/dialect"
"github.com/primandproper/platform-go/v12/database/querygen"
)
func main() {
queries := querygen.For(dialect.SQLite).StandardCRUD("things",
[]string{querygen.IDColumn, querygen.ArchivedAtColumn},
querygen.WithEntity("Thing", "Things"))
file := querygen.RenderFile(queries)
// Just the annotations, to keep the example short.
for line := range strings.SplitSeq(file, "\n") {
if strings.HasPrefix(line, "-- name:") {
fmt.Println(line)
}
}
}
Output: -- name: CreateThing :exec -- name: GetThing :one -- name: CheckThingExistence :one -- name: ListThings :many -- name: ArchiveThing :execrows
Types ¶
type Generator ¶
type Generator struct {
// contains filtered or unexported fields
}
Generator emits sqlc input for one SQL dialect.
The dialect is bound to the value rather than passed to each call, because a fragment and the statement it lands in have to agree about which server will parse them. A Postgres COLLATE "C" inside MySQL is a syntax error, which is the good case; a Postgres ILIKE has no SQLite spelling at all and the substitute differs in what it folds, which is the bad one. Binding the dialect to the value is what makes a mixed pair unrepresentable rather than merely discouraged.
Every method that emits SQL hangs off this type, including the ones whose output is currently identical on all three dialects. A caller should not have to know which fragments happen to be portable this week, and a divergence found later — the archived-row toggle was portable until sqlc's type inference wanted a cast — should be a change to one method body rather than a change to the package's surface.
func For ¶
For returns a Generator emitting d's SQL.
It panics on a dialect outside the supported set, in the manner of the rest of this package: the argument is a constant in a generator binary, so an unsupported dialect is a typo a build should stop for rather than a condition a caller could do anything with. The panic value is an error wrapping dialect.ErrUnsupported. A caller holding a dialect that came from configuration rather than a literal can ask dialect.Dialect.Valid first, and report the rejection in whatever terms its own users understand.
Example ¶
The dialect decides the SQL and not the shape: the same table yields the same query names with the same arguments on all three, so the application code over the generated methods is written once.
package main
import (
"fmt"
"strings"
"github.com/primandproper/platform-go/v12/database/dialect"
"github.com/primandproper/platform-go/v12/database/querygen"
)
func main() {
for _, d := range []dialect.Dialect{dialect.Postgres, dialect.MySQL, dialect.SQLite} {
for line := range strings.SplitSeq(querygen.For(d).ReindexScanQuery("things"), "\n") {
if strings.HasPrefix(line, "ORDER BY") {
fmt.Printf("%s: %s\n", d, line)
}
}
}
}
Output: postgres: ORDER BY things.id COLLATE "C" mysql: ORDER BY CAST(things.id AS BINARY) sqlite: ORDER BY things.id COLLATE BINARY
func (*Generator) ContainsCondition ¶
ContainsCondition renders a case-insensitive substring match of column against a bound argument, for a search query's own WHERE predicate.
It takes the column rather than returning an operator for the caller to prefix, because only two of the three dialects have an operator that folds case on its own. The other two fold both sides explicitly, which is a predicate rather than a suffix — see Generator.substringMatch for what each dialect gets and for the one input where they disagree about the answer.
func (*Generator) CursorCondition ¶
CursorCondition renders the keyset predicate: rows strictly after the cursor.
An absent cursor coalesces to the empty string rather than being handled by a second query, which is what keeps the first page and the fiftieth page the same statement. It works because id sorts by creation time and no id is empty.
func (*Generator) CursorLimitClause ¶
CursorLimitClause renders the ordering and page size a keyset walk needs.
The ORDER BY is not decoration. A cursor names a position in an order, so a paginated query without the matching ORDER BY returns rows in whatever order the planner found convenient, and the next page's cursor names a position in an order that no longer holds — pages that skip rows and repeat others, with nothing reporting an error.
func (*Generator) CursorPaginationFragment ¶
CursorPaginationFragment renders the cursor predicate and the ordering together, for a query that does its own filtering and only wants the keyset half.
The predicate arrives prefixed with AND, because the only place it belongs is the tail of a WHERE clause that already has one.
func (*Generator) FilterConditions ¶
FilterConditions renders a filtered list query's WHERE clause: the filtering.QueryFilter window over whichever of the convention columns the table has, then any conditions the caller adds, then the cursor predicate.
It is the whole clause, not an addendum. A caller that opens its own WHERE with archived_at IS NULL and appends this one gets a query where include_archived cannot do anything, since the first predicate has already excluded every row the flag would admit — and nothing about such a query looks wrong. Owning the clause is what keeps that from being expressible.
conditions are rendered verbatim, one per line. They are the caller's SQL: this package does not parse them and cannot vet them — nor, therefore, can it tell whether they are the dialect g emits for.
Example ¶
The fragment builders are there for the queries a table needs beyond the standard set — a search, a scoped list — so that those agree with the standard ones about what a filter means, and speak the same dialect while they do it.
package main
import (
"fmt"
"github.com/primandproper/platform-go/v12/database/dialect"
"github.com/primandproper/platform-go/v12/database/querygen"
)
func main() {
g := querygen.For(dialect.Postgres)
columns := []string{querygen.IDColumn, querygen.CreatedAtColumn, querygen.ArchivedAtColumn}
fmt.Println(g.FilterConditions("things", columns, g.ContainsCondition("things.name", "name_query")))
}
Output: things.created_at > COALESCE(sqlc.narg(created_after), (SELECT CURRENT_TIMESTAMP - '999 years'::INTERVAL)) AND things.created_at < COALESCE(sqlc.narg(created_before), (SELECT CURRENT_TIMESTAMP + '999 years'::INTERVAL)) AND (COALESCE(sqlc.narg(include_archived), false)::boolean OR things.archived_at IS NULL) AND things.name ILIKE '%' || sqlc.arg(name_query)::text || '%' AND things.id > COALESCE(sqlc.narg(cursor), '')
func (*Generator) FilterCountSelect ¶
func (g *Generator) FilterCountSelect(table string, columns, joins []string, conditions ...string) string
FilterCountSelect renders the scalar subquery counting the rows the same filter matches, aliased filtered_count.
It is a subquery in the SELECT list rather than a second round trip because filtering.QueryFilteredResult wants the page and its counts together, and a count issued separately counts a table that has moved on since the page was read.
The cursor predicate is deliberately absent: filtered_count answers "how many rows match this filter", which does not change as the caller walks through them. Including it would count the rows remaining after the cursor, and a total that shrinks with every page is a progress bar that never fills.
Because the count rides on the rows, a page with no rows carries no count. A caller reporting counts for an empty page has to supply the zero itself, which is what filtering.NewQueryFilteredResult taking them as arguments allows.
func (*Generator) IndexStampQuery ¶
IndexStampQuery builds the write that maintains last_indexed_at: one UPDATE stamping every id it is handed.
It is the other half of ReindexScanQuery. The column is what marks a table as one search/sync mirrors and what the reindex scan reads, and until something wrote it the scan walked a column nothing maintained — so the statement that maintains it is emitted from the same column list, rather than being left to each consumer to hand-write once per indexed table.
The ids arrive as a set bound in one argument rather than one statement per id, because the caller is a batching.Buffer flushing a coalesced set: one statement per flush is the entire reason the write is buffered. See searchsync.NewStampBuffer, which is what a Syncer stamps through. How that set reaches the server differs by dialect and the Go signature does not — see Generator.idSetPredicate.
There is no owner predicate and no archived_at predicate, and both omissions are deliberate. This is the search sync's own machinery servicing itself — it stamps the rows an index accepted, which it named explicitly — rather than a consumer read that owes a tenancy scope. And a row whose archived_at is set is a row the Syncer deleted from the index rather than stamped, so a predicate excluding it would be one that never fires while making the statement unemittable for a table that has no soft delete.
func (*Generator) ReindexScanQuery ¶
ReindexScanQuery builds the keyset walk a search reindex reads its source through.
It returns IDs rather than rows on purpose. A Scanner and a Fetcher both have to produce the same document for the same row, and the cheapest way to guarantee that is to have one of them call the other: the scan names the next page of IDs and the fetch — the same one the change feed uses — turns them into documents. Selecting rows here would be a second row-to-document transform, and two transforms that are supposed to agree are two transforms that can drift.
The ordering is a byte comparison rather than the database's default collation, on every dialect, for a reason the merge in search/sync's pruner makes unforgiving — see Generator.byteOrdered.
func (*Generator) StandardCRUD ¶
StandardCRUD emits the queries every table following this module's row conventions needs: create, get, exists, list, update, archive, the id scan a search reindex walks, and the stamp that maintains the column the scan reads.
columns is the table's full column list, in the order the emitted SELECTs should list them, and it decides which queries appear. A table without archived_at gets no archive; one without last_indexed_at gets neither the reindex scan nor the stamp; one with nothing a caller may assign gets no create and no update. The alternative — emitting a query that references a column the table does not have — is SQL that fails at sqlc generate for a reason that reads as a schema problem.
It panics rather than returning an error, in the manner of regexp.MustCompile. Its arguments are string literals in a generator binary, so every way it can fail is a typo that a build should stop for, and there is no caller who could do anything with an error that the panic does not do more loudly. The panic value is an error wrapping dialect.ErrInvalidIdentifier, ErrMissingIDColumn, or ErrDuplicateQueryName.
Which queries appear does not depend on the dialect, and neither do their names. A table generated for Postgres and the same table generated for SQLite yield the same set of sqlc methods with the same signatures — bar the two places sqlc's own type inference differs, which the package comment names — so the application code above them is written once. What differs is the SQL under each name.
Example ¶
A table's generator names the dialect, the table and its columns, and the standard set follows from them. What a consumer writes per table is the schema; what this package writes is the conventions.
package main
import (
"fmt"
"github.com/primandproper/platform-go/v12/database/dialect"
"github.com/primandproper/platform-go/v12/database/querygen"
)
func main() {
queries := querygen.For(dialect.Postgres).StandardCRUD("webhooks", []string{
querygen.IDColumn,
"name",
"url",
querygen.BelongsToAccountColumn,
querygen.CreatedAtColumn,
querygen.LastUpdatedAtColumn,
querygen.ArchivedAtColumn,
},
querygen.WithEntity("Webhook", "Webhooks"),
querygen.WithOwnership(querygen.BelongsToAccountColumn),
)
for _, query := range queries {
fmt.Printf("%s %s\n", query.Annotation.Name, query.Annotation.Type)
}
}
Output: CreateWebhook :exec GetWebhook :one CheckWebhookExistence :one ListWebhooks :many UpdateWebhook :execrows ArchiveWebhook :execrows
func (*Generator) TotalCountSelect ¶
func (g *Generator) TotalCountSelect(table string, columns, joins []string, conditions ...string) string
TotalCountSelect renders the scalar subquery counting the rows in scope regardless of the filter window, aliased total_count.
It applies the same archived handling as the filter — not an unconditional archived_at IS NULL — so that filtered_count can never exceed total_count. A pair of counts where the subset is larger than the set is the kind of number that gets noticed a week later by whoever is reconciling them.
type JoinStatement ¶
type JoinStatement struct {
// JoinTarget is the table being joined in.
JoinTarget string
// TargetColumn is the column on JoinTarget the join matches.
TargetColumn string
// OnTable and OnColumn name the side already in the query.
OnTable string
OnColumn string
}
JoinStatement is one join in a filtered count's FROM clause: the table being joined in, the column on it, and the already-present table and column it is matched against.
func (JoinStatement) String ¶
func (j JoinStatement) String() string
String renders the join clause. An inner join on an equality is the one piece of SQL in this package that all three dialects spell identically, so it is a plain String rather than something a Generator has to render.
type Option ¶
type Option func(*settings)
Option adjusts what StandardCRUD emits.
func WithDatabaseOwned ¶
WithDatabaseOwned names further columns the database fills in, beyond the four this package already knows about, excluding them from both INSERT and UPDATE.
func WithEntity ¶
WithEntity sets the singular and plural entity names the default query names are built from — WithEntity("valid instrument", "valid instruments") is written as WithEntity("ValidInstrument", "ValidInstruments").
Both default to the table name in upper camel case, which makes the default names correct but plural throughout: GetValidInstruments reads one row. The singular is not derived from the table, because deriving it means guessing whether the table is statuses, indices, or data, and a generator that guesses its callers' method names is a generator whose output has to be read to be trusted.
func WithImmutable ¶
WithImmutable names columns that are set once at insert and never assigned again — the row's creator, the parent it hangs off — excluding them from UPDATE only.
func WithNullable ¶
WithNullable names columns an INSERT or an UPDATE may set to NULL, binding them with sqlc.narg rather than sqlc.arg so the generated Go parameter is a pointer instead of a value.
It cannot be derived. A column list is names, and whether the column behind one is NOT NULL lives in the schema this package never reads. Nor does getting it wrong stop a build: sqlc generates against the schema, so an omitted nullable column yields a parameter that cannot express the NULL the column accepts, and a column named here that is NOT NULL yields one that can express a NULL the database will reject at runtime. Both are quiet, which is why they are declared at the table rather than inferred from one.
Reads are unaffected — a SELECT lists the column either way.
func WithOmitted ¶
func WithOmitted(queries ...StandardQuery) Option
WithOmitted drops queries from the set, for a table whose rows are not addressable the way the whole set assumes.
Not every table following these conventions is a resource. A child row written as part of its parent and only ever read through it has no caller for a get by id, an exists, or a list, and emitting them anyway produces generated methods nobody calls next to a read path that answers without whatever scoping the parent's own queries apply — the sort of query that is found later by someone looking for a convenient way to fetch a row.
It only subtracts. What StandardCRUD emits stays a subset of what the column list justifies, so a table without archived_at still cannot acquire an Archive and this option cannot conjure a query the columns do not support. Naming a query the columns already exclude is not an error; it says the same thing twice.
Omitting everything yields an empty slice, which RenderFile renders as the empty string rather than a file with no queries in it.
func WithOwnership ¶
WithOwnership scopes the single-row queries and the list to an owner column — BelongsToAccountColumn, conventionally — so that every one of them takes the owner as an argument and a row belonging to someone else is not found rather than found and returned.
It is opt-in rather than inferred from the column set. Inferring it would mean that renaming a column, or building a table's generator from a column list that happens to omit one, silently widens who can read every row — the class of change that looks like nothing in a diff.
The column is also excluded from UPDATE, since a row that can reassign its own owner makes the scope on every other query a formality.
func WithQueryName ¶
func WithQueryName(query StandardQuery, name string) Option
WithQueryName renames one query, for a consumer whose existing generated code spells it differently.
type Query ¶
type Query struct {
Content string
Annotation QueryAnnotation
}
Query is one annotated statement: the SQL, and the annotation that tells sqlc what to make of it.
type QueryAnnotation ¶
QueryAnnotation is the `-- name: X :one` line sqlc reads above a query. Name becomes the generated Go method's name, so it has to be unique across every file in a sqlc package, not merely within its own file.
type QueryType ¶
type QueryType string
QueryType is the sqlc annotation suffix declaring what a query returns. It is the half of the annotation sqlc reads to decide the generated method's signature, so a mismatch between it and the SQL is a compile error in the generated package rather than a runtime surprise.
const ( // ExecType returns nothing. It is the annotation for an INSERT whose caller // does not need to know whether a row was written, because a failed one // raises rather than returning zero. ExecType QueryType = ":exec" // ExecRowsType returns the number of rows affected. It is the annotation for // the writes whose row count is the answer — an UPDATE or an archival that // matched nothing is how a caller learns the row was already gone. ExecRowsType QueryType = ":execrows" // ManyType returns a slice of rows. ManyType QueryType = ":many" // OneType returns exactly one row, and an error when there is none. OneType QueryType = ":one" )
type StandardQuery ¶
type StandardQuery int
StandardQuery names one of the queries StandardCRUD emits, for renaming it.
const ( // CreateQuery inserts a row, taking a value for every column the database // does not own. CreateQuery StandardQuery = iota // GetQuery reads one unarchived row by id. GetQuery // ExistsQuery reports whether GetQuery would find a row, without reading it. ExistsQuery // ListQuery reads a filtered, cursor-paginated page along with the two // counts filtering.QueryFilteredResult carries. ListQuery // UpdateQuery assigns every mutable column and stamps last_updated_at. UpdateQuery // ArchiveQuery soft-deletes a row. ArchiveQuery // ScanIDsForReindexQuery walks ids in byte order for a search reindex. ScanIDsForReindexQuery // MarkAsIndexedQuery stamps last_indexed_at on every id it is handed, which // is what a search/sync Syncer flushes through once the index has accepted // those documents. MarkAsIndexedQuery )
func (StandardQuery) String ¶
func (s StandardQuery) String() string
String names the query, for error messages.